// ============================================================================ 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" ) var version = "1.19.3" 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 done chan bool var r1, r2, r3 *regexp.Regexp 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(0); } } 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 ] [-o ] [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 exec options")) P(Cw(" -c 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(0) } GO = path path, err = exec.LookPath("goimports") // ·············································· check for 'goimports' if err != nil { fmt.Println(Crb("Error: ") + Cwb("goimports") + " not found") os.Exit(0) } 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(0) } } file := name + ".go" if !fileExists(file) { P(Crb("file not found: " + file)) os.Exit(0) } var ot int64 = 0 var st int = 1 var xmd *exec.Cmd = nil if (!*opt_1 && !*opt_a && !*opt_u) { PN("\033[2J\033[1;1H") } info() if *opt_a { // ······························································· build for other platforms targetversion:=gettargetversion(file) 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(file) 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) } r1 = regexp.MustCompile("^\\.") r2 = regexp.MustCompile("build\\.go") r3 = regexp.MustCompile("\\.go$") for { // ------------------------------------------------------------------------------------------- main loop lmf := lmf("./") var dt int64 = 0 if fileExists(name) { dt = lmf - filemodtime(name) } else { dt = 1 } if (dt > 0 && ot != lmf) || st == 1 { // ·············································· new version detected if st == 0 && !*opt_1{ PN("\033[2J\033[1;1H") } P(Cc("--- start compiling " + file + " ---")) stopRunning(xmd) o := "" // ········································································· increase build number readFile, err := os.Open("build.go") if err == nil { defer readFile.Close() backup("build.go") fileScanner := bufio.NewScanner(readFile) fileScanner.Split(bufio.ScanLines) r := regexp.MustCompile("var\\s+build\\s*=\\s*\"(.*)\"$") for fileScanner.Scan() { line := fileScanner.Text() s := r.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) } targetversion:="" readFile, err = os.Open(file) if err == nil { fileScanner := bufio.NewScanner(readFile) fileScanner.Split(bufio.ScanLines) r := regexp.MustCompile("var\\s+version\\s*=\\s*\"(.*)\"$") for fileScanner.Scan() { line := fileScanner.Text() s := r.FindStringSubmatch(line) if len(s) > 0 { targetversion=s[1] } } readFile.Close() } if (targetversion=="") { PE("target version not found") os.Exit(0) } 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 && !*opt_a { 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) } } ot = lmf } if *opt_1 { os.Exit(0) } st = 0 time.Sleep(500 * time.Millisecond) } } 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() } // ===================================================================================================== SUPPORT func gettargetversion(file string) string { // ------------------------------------------- detect target version targetversion:="" readFile, err := os.Open(file) if err == nil { fileScanner := bufio.NewScanner(readFile) fileScanner.Split(bufio.ScanLines) r := regexp.MustCompile("var\\s+version\\s*=\\s*\"(.*)\"$") for fileScanner.Scan() { line := fileScanner.Text() s := r.FindStringSubmatch(line) if len(s) > 0 { targetversion=s[1] } } readFile.Close() } if (targetversion=="") { PE("target version not found") os.Exit(0) } return targetversion } func lmf(dir string) int64 { // -------------------------------------- get last modification date from .go files var tmax int64 = 0 files, err := os.ReadDir(dir) if err == nil { for _, file := range files { if r3.MatchString(file.Name()) { if !r1.MatchString(file.Name()) && !r2.MatchString(file.Name()) { tf := filemodtime(file.Name()) if tf > tmax { tmax = tf } } } } } return tmax } 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.Kill() if err == nil { cmd.Wait() P("process stopped:", 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