453 lines
14 KiB
Go
453 lines
14 KiB
Go
// ============================================================================ simple golang builder (mwx'2026)
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/AlecAivazis/survey/v2"
|
|
"github.com/fsnotify/fsnotify"
|
|
)
|
|
|
|
var version = "1.20.1"
|
|
|
|
var UPDATEURL = "http://gozilla.fhi.mpg.de/gbld"
|
|
|
|
var oses = []string{"darwin", "linux", "windows"}
|
|
var arches = []string{"arm64", "amd64"}
|
|
|
|
var GO string
|
|
var GOIMPORTS string
|
|
var xmd *exec.Cmd
|
|
|
|
var done chan bool
|
|
var r1 = regexp.MustCompile(`^\.`)
|
|
var r2 = regexp.MustCompile(`build\.go`)
|
|
var r3 = regexp.MustCompile(`\.go$`)
|
|
var r4 = regexp.MustCompile(`var\s+build\s*=\s*"(.*)"$`)
|
|
var r5 = regexp.MustCompile(`(?i)var\s+version\s*=\s*"(.*)"$`)
|
|
|
|
func main() { // ========================================================================================== main
|
|
|
|
checkforupdate(UPDATEURL)
|
|
|
|
if len(os.Args) == 1 { // read predefined builds
|
|
c, err := os.ReadFile("gbld.menu.json")
|
|
if err == nil {
|
|
var sel []string
|
|
var buildjson map[string][]string
|
|
|
|
err := json.Unmarshal([]byte(c), &buildjson)
|
|
if err == nil {
|
|
for k := range buildjson {
|
|
sel = append(sel, k)
|
|
}
|
|
tmp := ""
|
|
err = survey.AskOne(&survey.Select{Message: "Select build", Options: sel}, &tmp)
|
|
os.Args = []string{"gbld"}
|
|
os.Args = append(os.Args, buildjson[tmp]...)
|
|
} else {
|
|
PE(err.Error())
|
|
os.Exit(1)
|
|
}
|
|
} else {
|
|
os.Args = []string{"gbld","-b","-i","-1",dirName()}
|
|
}
|
|
}
|
|
|
|
opt_b := flag.Bool("b", false, "")
|
|
opt_1 := flag.Bool("1", false, "")
|
|
opt_i := flag.Bool("i", false, "")
|
|
opt_x := flag.Bool("x", false, "")
|
|
opt_c := flag.String("c", "", "")
|
|
opt_o := flag.String("o", "", "")
|
|
opt_a := flag.Bool("a", false, "")
|
|
opt_v := flag.Bool("v", false, "")
|
|
opt_h := flag.Bool("h", false, "")
|
|
opt_u := flag.Bool("u", false, "")
|
|
|
|
flag.Usage = func() {
|
|
P()
|
|
info()
|
|
P()
|
|
P(Cw("Usage:"))
|
|
P(Cy(" gbld [-b] [-1] [-i] [-c <CMD>] [-o <OPTIONS>] [build name]"))
|
|
P(Cy(" gbld [-a] [-u]"))
|
|
P(Cw(" -b build only"))
|
|
P(Cw(" -1 run only once"))
|
|
P(Cw(" -i run gpimports before build"))
|
|
P(Cw(" -o <OPTIONS> exec options"))
|
|
P(Cw(" -c <CMD> run command after build"))
|
|
P(Cw(" -v show version"))
|
|
P(Cw(" -h show help"))
|
|
P(Cw(" -a build for all platforms"))
|
|
P(Cw(" -u upload builds to gozilla"))
|
|
P()
|
|
P(Cw(" · if build is supplied the current directory name will be used"))
|
|
P()
|
|
P(Cw(" · if no option supplied try to read '") + Cy("gbld.menu.json") + Cw("'"))
|
|
P(Cm(` {`))
|
|
P(Cm(` "build": ["-b","gbld"],`))
|
|
P(Cm(` "help": ["-h"]`))
|
|
P(Cm(` }`))
|
|
P()
|
|
P(Cw(" · if no option is supplied and no '") + Cy("gbld.menu.json") + Cw("' is available"))
|
|
P(Cw(" the current directory name with ") + Cy("-b -i -1") + Cw(" as options will be used"))
|
|
P()
|
|
}
|
|
|
|
flag.Parse()
|
|
|
|
path, err := exec.LookPath("go") // ··························································· check for 'go'
|
|
if err != nil {
|
|
fmt.Println(Crb("Error: ") + Cwb("go") + " not found")
|
|
os.Exit(1)
|
|
}
|
|
GO = path
|
|
|
|
path, err = exec.LookPath("goimports") // ·············································· check for 'goimports'
|
|
if err != nil {
|
|
fmt.Println(Crb("Error: ") + Cwb("goimports") + " not found")
|
|
os.Exit(1)
|
|
}
|
|
GOIMPORTS = path
|
|
|
|
if *opt_v { // ·············································································· show version
|
|
info()
|
|
os.Exit(0)
|
|
}
|
|
|
|
if *opt_h { // ····················································································· show help
|
|
flag.Usage()
|
|
os.Exit(0)
|
|
}
|
|
|
|
name := flag.Arg(0) // ································································ command line arguments
|
|
if len(name) == 0 {
|
|
name=dirName()
|
|
if len(name) == 0 {
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
file := name + ".go"
|
|
if !fileExists(file) {
|
|
P(Crb("file not found: " + file))
|
|
os.Exit(1)
|
|
}
|
|
|
|
if (!*opt_1 && !*opt_a && !*opt_u) {
|
|
PN("\033[2J\033[1;1H")
|
|
}
|
|
info()
|
|
|
|
if *opt_a { // ······························································· build for other platforms
|
|
targetversion:=gettargetversion()
|
|
P(Ccb("build for all platforms"))
|
|
for _, osys := range oses {
|
|
for _, arch := range arches {
|
|
ofile:="bin/"+name+"_"+targetversion+"_"+osys+"_"+arch
|
|
P(Cy(ofile))
|
|
|
|
cmd := exec.Command(GO, "build", "-o", ofile, name)
|
|
cmd.Env = append(os.Environ(), "GOOS="+osys, "GOARCH="+arch)
|
|
cmd.Run()
|
|
}
|
|
}
|
|
os.WriteFile("bin/version.txt", []byte(targetversion), 0644)
|
|
}
|
|
|
|
if *opt_u { // ······································································ upload builds to gozilla
|
|
targetversion:=gettargetversion()
|
|
P(Ccb("upload builds to gozilla"))
|
|
|
|
for _, osys := range oses {
|
|
for _, arch := range arches {
|
|
ofile:="bin/"+name+"_"+targetversion+"_"+osys+"_"+arch
|
|
_, err := os.Stat(ofile)
|
|
if err == nil {
|
|
cmd := exec.Command("ssh","root@gozilla","rm", "/db/www/"+name+"/*_"+osys+"_"+arch )
|
|
P(Cy(cmd))
|
|
cmd.Run()
|
|
|
|
cmd = exec.Command("scp",ofile, "root@gozilla:/db/www/"+name)
|
|
P(Cy(cmd))
|
|
cmd.Run()
|
|
}
|
|
}
|
|
}
|
|
_, err := os.Stat("bin/version.txt")
|
|
if err == nil {
|
|
cmd := exec.Command("scp","bin/version.txt", "root@gozilla:/db/www/"+name)
|
|
P(Cy(cmd))
|
|
cmd.Run()
|
|
}
|
|
}
|
|
|
|
if (*opt_a || *opt_u) { os.Exit(0) }
|
|
|
|
// Initial build
|
|
buildAndRun(name, file, *opt_b, *opt_1, *opt_i, *opt_c, *opt_o, *opt_x)
|
|
|
|
if *opt_1 {
|
|
os.Exit(0)
|
|
}
|
|
|
|
// Set up fsnotify watcher
|
|
watcher, err := fsnotify.NewWatcher()
|
|
if err != nil {
|
|
PE(err.Error())
|
|
os.Exit(1)
|
|
}
|
|
defer watcher.Close()
|
|
|
|
err = watcher.Add(".")
|
|
if err != nil {
|
|
PE(err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
events := make(chan bool)
|
|
go func() {
|
|
for {
|
|
select {
|
|
case event, ok := <-watcher.Events:
|
|
if !ok {
|
|
return
|
|
}
|
|
if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create {
|
|
fileName := filepath.Base(event.Name)
|
|
if r3.MatchString(fileName) && !r1.MatchString(fileName) && !r2.MatchString(fileName) {
|
|
events <- true
|
|
}
|
|
}
|
|
case err, ok := <-watcher.Errors:
|
|
if !ok {
|
|
return
|
|
}
|
|
PE(err.Error())
|
|
}
|
|
}
|
|
}()
|
|
|
|
var timer *time.Timer
|
|
for {
|
|
select {
|
|
case <-events:
|
|
if timer != nil {
|
|
timer.Stop()
|
|
}
|
|
timer = time.AfterFunc(100*time.Millisecond, func() {
|
|
buildAndRun(name, file, *opt_b, *opt_1, *opt_i, *opt_c, *opt_o, *opt_x)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func buildAndRun(name, file string, opt_b, opt_1, opt_i bool, opt_c, opt_o string, opt_x bool) {
|
|
PN("\033[2J\033[1;1H")
|
|
P(Cc("--- start compiling " + file + " ---"))
|
|
|
|
stopRunning(xmd)
|
|
|
|
o := "" // ········································································· increase build number
|
|
func() {
|
|
readFile, err := os.Open("build.go")
|
|
if err == nil {
|
|
defer readFile.Close()
|
|
backup("build.go")
|
|
fileScanner := bufio.NewScanner(readFile)
|
|
fileScanner.Split(bufio.ScanLines)
|
|
for fileScanner.Scan() {
|
|
line := fileScanner.Text()
|
|
s := r4.FindStringSubmatch(line)
|
|
if len(s) > 0 {
|
|
b := Atoi(s[1]) + 1
|
|
o = o + "var build = \"" + Itoa(b) + "\"\n"
|
|
P("build:", s[1], "->", b)
|
|
} else {
|
|
o = o + line + "\n"
|
|
}
|
|
}
|
|
os.WriteFile("build.go", []byte(o), 0644)
|
|
}
|
|
}()
|
|
|
|
_ = gettargetversion()
|
|
|
|
if opt_i { // ············································································· run goimports
|
|
re := regexp.MustCompile(`(?s)import\s*\((.*?)\)`)
|
|
files, err := os.ReadDir(".")
|
|
if err != nil { fmt.Printf("Error: %v\n", err); return }
|
|
|
|
for _, f := range files {
|
|
if !f.IsDir() && strings.HasSuffix(f.Name(), ".go") {
|
|
fileName := f.Name()
|
|
cmd := exec.Command("goimports", fileName)
|
|
output, err := cmd.Output()
|
|
if err != nil { continue }
|
|
origContent, _ := os.ReadFile(fileName)
|
|
tempMatch := re.FindStringSubmatch(string(output))
|
|
if len(tempMatch) < 2 { continue }
|
|
newImportBlock := tempMatch[1]
|
|
updatedContent := ReplaceFirst(re,string(origContent),fmt.Sprintf("import (%s)", newImportBlock))
|
|
if (string(origContent) != updatedContent) {
|
|
backup(fileName)
|
|
PF("%s %s\n",Cw("imports updated:"),Cmb(fileName))
|
|
os.WriteFile(fileName, []byte(updatedContent), 0644)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
cmd := exec.Command("go", "build", "-o", "bin/"+name, name)
|
|
PF("%s %s\n", Cw("running:"), Cyb("go build "+name))
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
|
|
if err != nil { // ·········································································· build failed
|
|
PN(Cy(string(out)))
|
|
P(Cr("--- build failed: " + name + " ---"))
|
|
} else { // ·············································································· build succeeded
|
|
P(Cb("--- compiling done: " + name + " ---"))
|
|
|
|
if len(opt_c) > 0 { // ··························································· execute post command
|
|
params := strings.Fields(opt_c)
|
|
app := params[0]
|
|
args := params[1:]
|
|
|
|
P(Cw(opt_c))
|
|
xmd = exec.Command(app, args...)
|
|
xmd.Stdout = os.Stdout
|
|
xmd.Stdin = os.Stdin
|
|
xmd.Stderr = os.Stderr
|
|
xmd.Run()
|
|
}
|
|
|
|
if !opt_b {
|
|
stopRunning(xmd)
|
|
|
|
args := []string{}
|
|
if len(opt_o) > 0 {
|
|
args = strings.Fields(opt_o)
|
|
}
|
|
|
|
xmd = exec.Command("./bin/"+name, args...) // ······································· start new build
|
|
var stdoutBuf, stderrBuf bytes.Buffer
|
|
xmd.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf)
|
|
xmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf)
|
|
xmd.Start()
|
|
|
|
PF("%s %d\n", Cw("process started:"), xmd.Process.Pid)
|
|
}
|
|
|
|
P(Cg("--- end building: " + name + " ---"))
|
|
if opt_x {
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
}
|
|
|
|
func backup(src string) { // ---------------------------------------------------------------------- backup files
|
|
sourceFile,_ := os.Open(src)
|
|
defer sourceFile.Close()
|
|
dst:=SF("./tmp/%s.%s",src,time.Now().Format("20060102150405"))
|
|
destFile,_ := os.Create(dst)
|
|
defer destFile.Close()
|
|
io.Copy(destFile, sourceFile)
|
|
destFile.Sync()
|
|
destFile.Close()
|
|
}
|
|
|
|
func gettargetversion() string { // ------------------------------------------- detect target version
|
|
files, err := filepath.Glob("*.go")
|
|
if err != nil {
|
|
PE(SF("Error listing files: %v", err))
|
|
os.Exit(1)
|
|
}
|
|
|
|
targetversion := ""
|
|
for _, file := range files {
|
|
readFile, err := os.Open(file)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
fileScanner := bufio.NewScanner(readFile)
|
|
fileScanner.Split(bufio.ScanLines)
|
|
for fileScanner.Scan() {
|
|
line := fileScanner.Text()
|
|
s := r5.FindStringSubmatch(line)
|
|
if len(s) > 0 {
|
|
targetversion = s[1]
|
|
readFile.Close()
|
|
return targetversion
|
|
}
|
|
}
|
|
readFile.Close()
|
|
}
|
|
|
|
if targetversion == "" {
|
|
PE("target version not found")
|
|
os.Exit(1)
|
|
}
|
|
|
|
return targetversion
|
|
}
|
|
|
|
func stopRunning(cmd *exec.Cmd) { // -------------------------------------------------- stop running old process
|
|
if cmd == nil || cmd.ProcessState != nil && cmd.ProcessState.Exited() || cmd.Process == nil {
|
|
return
|
|
}
|
|
|
|
err := cmd.Process.Signal(os.Interrupt)
|
|
if err == nil {
|
|
done := make(chan error, 1)
|
|
go func() { done <- cmd.Wait() }()
|
|
select {
|
|
case <-time.After(1 * time.Second):
|
|
cmd.Process.Kill()
|
|
<-done
|
|
case <-done:
|
|
}
|
|
P("process stopped:", cmd.Process.Pid)
|
|
} else {
|
|
cmd.Process.Kill()
|
|
cmd.Wait()
|
|
P("process stopped forcefully:", cmd.Process.Pid)
|
|
}
|
|
}
|
|
|
|
func fileExists(filename string) bool { // ------------------------------------------------ check if file exists
|
|
info, err := os.Stat(filename)
|
|
if os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
return !info.IsDir()
|
|
}
|
|
|
|
func dirName() string { // ------------------------------------------------------- get name of current directory
|
|
pwd,err := os.Getwd()
|
|
if err == nil {
|
|
return filepath.Base(pwd)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func info() { // ------------------------------------------------------------------------------------- show info
|
|
PF("%s (%s (%s), toolbox %s, %s)\n", Cwb("gbld - simple golang builder"),
|
|
Cgb("v"+version), Cgb(build), Cgb("v"+tbversion), Ccb("mwx'2026"))
|
|
}
|
|
|
|
// ========================================================================================================= END
|