nix-amer/internal/buildfile/reader.go

263 lines
5.5 KiB
Go

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 <dryRun> 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()
}