package buildfile import ( "bufio" "errors" "fmt" "github.com/xdrm-brackets/nix-amer/internal/clifmt" "github.com/xdrm-brackets/nix-amer/internal/instruction" "io" "regexp" "strings" "sync" "time" ) // ErrNullContext is raised when the given context is nil var ErrNullContext = errors.New("null context") // ErrNoParent is raised when there is an instruction but has no parent section var ErrNoParent = errors.New("missing parent section") // Reader is the buildfile reader type Reader struct { // Context is the linux distribution-specified execution context (package manager, service manager, etc) Context *instruction.ExecutionContext // Content is the instruction list Content map[string]*[]instruction.T } type execStatus struct { name string start time.Time stop time.Time stopped bool err error } type tableSection struct { name string instructions []execStatus } var reSection = regexp.MustCompile(`(?m)^\[\s*([a-z0-9_-]+)\s*\]$`) // NewReader creates a new reader for the specified build file and linux distribution func NewReader(ctx *instruction.ExecutionContext, buildfile io.Reader) (*Reader, error) { // fail on null context if ctx == nil { return nil, ErrNullContext } r := &Reader{ Context: ctx, Content: make(map[string]*[]instruction.T), } // add each line as instruction l, reader := 0, bufio.NewReader(buildfile) eof := false var section *[]instruction.T = nil // current section for { l++ if eof { break } // 1. read line until end line, err := reader.ReadString('\n') if err == io.EOF { if len(line) > 0 { eof = true } else { break } } else if err != nil { return nil, LineError{l, err} } line = strings.Trim(line, " \t\r\n") // 2. ignore newline & comments if len(line) < 1 || strings.ContainsAny(line[0:1], "#;") { continue } // 3. section if match := reSection.FindStringSubmatch(line); len(match) > 1 { // already in section sec := make([]instruction.T, 0) section = &sec r.Content[match[1]] = section continue } // 4. fail if no parent section if section == nil { return nil, ErrNoParent } // 5. create instruction inst, err := instruction.Parse(line) if err != nil { return nil, LineError{l, err} } // add to list *section = append(*section, inst) } return r, nil } // Execute the current buildfile instruction by instruction // if is set to TRUE, run on dry-run mode func (r *Reader) Execute(_dryRun ...bool) error { dryRun := true if len(_dryRun) < 1 || !_dryRun[0] { dryRun = false } // 1. update package list // err := r.Context.PackageManager.Fetch() // if err != nil { // return fmt.Errorf("cannot fetch packages | %s", err) // } // // 2. upgrade packages // err = r.Context.PackageManager.Upgrade() // if err != nil { // return fmt.Errorf("cannot upgrade | %s", err) // } refresh := make(chan bool, 1) wg := new(sync.WaitGroup) wgstatus := new(sync.WaitGroup) // 1. create status table + extract [pre] section if one table := make([]tableSection, 0) index := make(map[string]int, 0) var pre *[]instruction.T = nil var preTable *tableSection = nil for secname, sec := range r.Content { tableSec := tableSection{ name: secname, instructions: make([]execStatus, len(*sec), len(*sec)+1), } // for each instruction for i, inst := range *sec { tableSec.instructions[i].name = inst.Raw() } table = append(table, tableSec) index[secname] = len(table) - 1 // [pre] section if secname == "pre" { pre = sec preTable = &tableSec } // add one section wg.Add(len(*sec)) } // 2. launch status updater wgstatus.Add(1) go status(table, refresh, wgstatus) // 3. launch [pre] (it set) if pre != nil { execSection(pre, *r.Context, preTable, dryRun, refresh, wg) time.Sleep(time.Second * 2) } // 4. launch each other section for secname, sec := range r.Content { // do not launch pre again if secname == "pre" { continue } i, ok := index[secname] if !ok { continue } go execSection(sec, *r.Context, &table[i], dryRun, refresh, wg) } wg.Wait() close(refresh) wgstatus.Wait() return nil } func execSection(section *[]instruction.T, ctx instruction.ExecutionContext, tsec *tableSection, dryRun bool, refresher chan<- bool, wg *sync.WaitGroup) { for i, inst := range *section { tsec.instructions[i].start = time.Now() var err error = nil if dryRun { _, err = inst.DryRun(ctx) } else { _, err = inst.Exec(ctx) } tsec.instructions[i].stop = time.Now() tsec.instructions[i].stopped = true tsec.instructions[i].err = err refresher <- true wg.Done() } } func status(table []tableSection, refresher <-chan bool, wg *sync.WaitGroup) { for opened := true; true; _, opened = <-refresher { // 1. clean screen fmt.Printf("\033[H\033[2J") // 2. for each section for _, sec := range table { fmt.Printf("\n[ %s ]\n", sec.name) // 3. for each instruction for i, inst := range sec.instructions { if !inst.stopped { clifmt.Align(fmt.Sprintf("(%d) %s", i, clifmt.Color(0, inst.name))) fmt.Printf("%s\n", clifmt.Color(33, "processing")) continue } if inst.err != nil { clifmt.Align(fmt.Sprintf("(%d) %s", i, clifmt.Color(0, inst.name))) fmt.Printf("%s\n", clifmt.Color(31, inst.err.Error())) continue } else { clifmt.Align(fmt.Sprintf("(%d) %s", i, clifmt.Color(34, inst.name))) fmt.Printf("%ss\n", clifmt.Color(32, fmt.Sprintf("%.2f", inst.stop.Sub(inst.start).Seconds()))) } } } if !opened { break } } wg.Done() }