[mike@mwxm4]
This commit is contained in:
@@ -2,67 +2,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
var version = "1.20.1"
|
||||
var version = "1.21.0"
|
||||
|
||||
var UPDATEURL = "http://gozilla.fhi.mpg.de/gbld"
|
||||
var UPDATEURL = "https://gozilla.fhi.mpg.de/gbld"
|
||||
var UPLOADHOST = "root@gozilla"
|
||||
var UPLOADPATH = "/db/www"
|
||||
|
||||
var oses = []string{"darwin", "linux", "windows"}
|
||||
var arches = []string{"arm64", "amd64"}
|
||||
|
||||
var skipdirs = []string{"bin", "tmp", "vendor", "node_modules", "testdata"}
|
||||
|
||||
const KEEPBACKUPS = 20 // backups per file kept in tmp/
|
||||
|
||||
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*"(.*)"$`)
|
||||
var mu sync.Mutex // guards 'running'
|
||||
var running *proc
|
||||
|
||||
var r1 = regexp.MustCompile(`^\.`) // hidden file
|
||||
var r2 = regexp.MustCompile(`^build\.go$`) // own build counter
|
||||
var r3 = regexp.MustCompile(`\.go$`) // go source
|
||||
var r4 = regexp.MustCompile(`(?m)^(\s*var\s+build\s*=\s*")([^"]*)(")`) // build counter
|
||||
var r5 = regexp.MustCompile(`(?im)^\s*var\s+version\s*=\s*"([^"]*)"`) // target version
|
||||
var r6 = regexp.MustCompile(`(?s)import\s*\((.*?)\)`) // import block
|
||||
var r7 = regexp.MustCompile(`^[A-Za-z0-9._-]+$`) // safe build name
|
||||
|
||||
type opts struct { // ------------------------------------------------------------------------- command line options
|
||||
buildonly bool
|
||||
once bool
|
||||
imports bool
|
||||
exitafter bool
|
||||
postcmd string
|
||||
runargs string
|
||||
}
|
||||
|
||||
type proc struct { // ------------------------------------------------------ started build, reaped by its own goroutine
|
||||
cmd *exec.Cmd
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
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()}
|
||||
}
|
||||
if len(os.Args) == 1 { // ······································· no arguments: read predefined builds or default
|
||||
os.Args = defaultargs()
|
||||
}
|
||||
|
||||
opt_b := flag.Bool("b", false, "")
|
||||
@@ -81,19 +87,20 @@ func main() { // ===============================================================
|
||||
info()
|
||||
P()
|
||||
P(Cw("Usage:"))
|
||||
P(Cy(" gbld [-b] [-1] [-i] [-c <CMD>] [-o <OPTIONS>] [build name]"))
|
||||
P(Cy(" gbld [-b] [-1] [-i] [-x] [-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(" -1 run only once, no watching (exit code: build error or exit code of the build)"))
|
||||
P(Cw(" -i run goimports before build"))
|
||||
P(Cw(" -x exit after the first successful build, leave the started build running"))
|
||||
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(Cw(" -u upload builds to " + UPLOADHOST))
|
||||
P()
|
||||
P(Cw(" · if build is supplied the current directory name will be used"))
|
||||
P(Cw(" · if no build name 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(` {`))
|
||||
@@ -104,23 +111,14 @@ func main() { // ===============================================================
|
||||
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()
|
||||
P(Cw(" · set ") + Cy("GBLD_NO_UPDATE=1") + Cw(" to skip the update check"))
|
||||
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
|
||||
o := opts{buildonly: *opt_b, once: *opt_1, imports: *opt_i, exitafter: *opt_x,
|
||||
postcmd: *opt_c, runargs: *opt_o}
|
||||
|
||||
if *opt_v { // ·············································································· show version
|
||||
info()
|
||||
@@ -132,9 +130,18 @@ func main() { // ===============================================================
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if os.Getenv("GBLD_NO_UPDATE") == "" { // ······································· check for a newer gbld version
|
||||
checkforupdate(UPDATEURL)
|
||||
}
|
||||
|
||||
GO = lookup("go") // ······················································· check for the required toolchain
|
||||
if o.imports {
|
||||
GOIMPORTS = lookup("goimports")
|
||||
}
|
||||
|
||||
name := flag.Arg(0) // ································································ command line arguments
|
||||
if len(name) == 0 {
|
||||
name=dirName()
|
||||
name = dirName()
|
||||
if len(name) == 0 {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
@@ -143,82 +150,48 @@ func main() { // ===============================================================
|
||||
|
||||
file := name + ".go"
|
||||
if !fileExists(file) {
|
||||
P(Crb("file not found: " + file))
|
||||
PE("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)
|
||||
info()
|
||||
buildall(name)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
info()
|
||||
uploadall(name)
|
||||
}
|
||||
|
||||
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 {
|
||||
if *opt_a || *opt_u {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Set up fsnotify watcher
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
catchsignals() // ································· stop the started build when gbld itself is asked to quit
|
||||
|
||||
ok := buildAndRun(name, file, o) // ································································ first build
|
||||
|
||||
if o.once { // ······················· no watching: report the build result and wait for the started process
|
||||
if !ok {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(waitRunning())
|
||||
}
|
||||
|
||||
watcher, err := fsnotify.NewWatcher() // ····································· watch the tree for .go changes
|
||||
if err != nil {
|
||||
PE(err.Error())
|
||||
PE("cannot create watcher", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
err = watcher.Add(".")
|
||||
if err != nil {
|
||||
PE(err.Error())
|
||||
if err := watchtree(watcher, "."); err != nil {
|
||||
PE("cannot watch directory", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
events := make(chan bool)
|
||||
trigger := make(chan struct{}, 1)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
@@ -226,221 +199,495 @@ func main() { // ===============================================================
|
||||
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
|
||||
if event.Op&fsnotify.Create == fsnotify.Create { // new directory: watch it as well
|
||||
if fi, err := os.Stat(event.Name); err == nil && fi.IsDir() {
|
||||
watchtree(watcher, event.Name)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Rename|fsnotify.Remove) == 0 {
|
||||
continue
|
||||
}
|
||||
base := filepath.Base(event.Name)
|
||||
if !r3.MatchString(base) || r1.MatchString(base) || r2.MatchString(base) {
|
||||
continue
|
||||
}
|
||||
select { // never block the watcher, one pending rebuild is enough
|
||||
case trigger <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
PE(err.Error())
|
||||
PE("watcher", err.Error())
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var timer *time.Timer
|
||||
for {
|
||||
select {
|
||||
case <-events:
|
||||
if timer != nil {
|
||||
timer.Stop()
|
||||
for range trigger { // ································· debounce, then build serially - never in parallel
|
||||
for settled := false; !settled; {
|
||||
select {
|
||||
case <-trigger:
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
settled = true
|
||||
}
|
||||
timer = time.AfterFunc(100*time.Millisecond, func() {
|
||||
buildAndRun(name, file, *opt_b, *opt_1, *opt_i, *opt_c, *opt_o, *opt_x)
|
||||
})
|
||||
}
|
||||
buildAndRun(name, file, o)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
func defaultargs() []string { // ------------------------------------------------- arguments if none were supplied
|
||||
c, err := os.ReadFile("gbld.menu.json")
|
||||
if err != nil {
|
||||
return []string{"gbld", "-b", "-i", "-1", dirName()}
|
||||
}
|
||||
|
||||
cmd := exec.Command("go", "build", "-o", "bin/"+name, name)
|
||||
var buildjson map[string][]string
|
||||
if err := json.Unmarshal(c, &buildjson); err != nil {
|
||||
PE("cannot parse gbld.menu.json", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
sel := []string{}
|
||||
for k := range buildjson {
|
||||
sel = append(sel, k)
|
||||
}
|
||||
slices.Sort(sel) // map order is random - keep the menu stable
|
||||
|
||||
tmp := ""
|
||||
if err := survey.AskOne(&survey.Select{Message: "Select build", Options: sel}, &tmp); err != nil {
|
||||
P(Crb("Interrupted."))
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
args, ok := buildjson[tmp]
|
||||
if !ok {
|
||||
PE("unknown build", tmp)
|
||||
os.Exit(1)
|
||||
}
|
||||
return append([]string{"gbld"}, args...)
|
||||
}
|
||||
|
||||
func buildAndRun(name, file string, o opts) bool { // ------------------------- build and (re)start it, ok on success
|
||||
if !o.once {
|
||||
PN("\033[2J\033[1;1H")
|
||||
}
|
||||
info()
|
||||
P(Cc("--- start compiling " + file + " ---"))
|
||||
|
||||
stopRunning()
|
||||
bumpbuild()
|
||||
|
||||
if o.imports {
|
||||
runimports()
|
||||
}
|
||||
|
||||
target := filepath.Join("bin", name+exesuffix(runtime.GOOS))
|
||||
PF("%s %s\n", Cw("running:"), Cyb("go build "+name))
|
||||
|
||||
cmd := exec.Command(GO, "build", "-o", target, ".")
|
||||
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 + " ---"))
|
||||
return false
|
||||
}
|
||||
|
||||
if len(opt_c) > 0 { // ··························································· execute post command
|
||||
params := strings.Fields(opt_c)
|
||||
app := params[0]
|
||||
args := params[1:]
|
||||
P(Cb("--- compiling done: " + name + " ---")) // ·········································· build succeeded
|
||||
|
||||
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)
|
||||
if len(o.postcmd) > 0 { // ······················································· execute post command
|
||||
params := strings.Fields(o.postcmd)
|
||||
P(Cw(o.postcmd))
|
||||
post := exec.Command(params[0], params[1:]...)
|
||||
post.Stdout = os.Stdout
|
||||
post.Stdin = os.Stdin
|
||||
post.Stderr = os.Stderr
|
||||
if err := post.Run(); err != nil {
|
||||
PE("post command failed", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if !o.buildonly { // ······················································· start the new build
|
||||
args := []string{}
|
||||
if len(o.runargs) > 0 {
|
||||
args = strings.Fields(o.runargs)
|
||||
}
|
||||
|
||||
cmd := exec.Command(target, args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
PE("cannot start "+target, err.Error())
|
||||
} else {
|
||||
p := &proc{cmd: cmd, done: make(chan struct{})}
|
||||
go func() { cmd.Wait(); close(p.done) }() // reap it as soon as it ends on its own
|
||||
mu.Lock()
|
||||
running = p
|
||||
mu.Unlock()
|
||||
PF("%s %d\n", Cw("process started:"), cmd.Process.Pid)
|
||||
}
|
||||
}
|
||||
|
||||
P(Cg("--- end building: " + name + " ---"))
|
||||
if o.exitafter { // leaves the started process running on purpose
|
||||
os.Exit(0)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
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 waitRunning() int { // ------------------------------------- wait for the started build, return its exit code
|
||||
mu.Lock()
|
||||
p := running
|
||||
running = nil
|
||||
mu.Unlock()
|
||||
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
<-p.done
|
||||
if p.cmd.ProcessState == nil {
|
||||
return 0
|
||||
}
|
||||
return p.cmd.ProcessState.ExitCode()
|
||||
}
|
||||
|
||||
func gettargetversion() string { // ------------------------------------------- detect target version
|
||||
files, err := filepath.Glob("*.go")
|
||||
func bumpbuild() { // ------------------------------------------------------------------ increase build number
|
||||
c, err := os.ReadFile("build.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()
|
||||
return // no build.go, nothing to count
|
||||
}
|
||||
|
||||
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 {
|
||||
m := r4.FindSubmatch(c)
|
||||
if m == 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)
|
||||
old := string(m[2])
|
||||
b := Atoi(old) + 1
|
||||
|
||||
if err := backup("build.go"); err != nil { // never touch a file we could not save first
|
||||
PE("backup of build.go failed", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile("build.go", r4.ReplaceAll(c, []byte("${1}"+Itoa(b)+"${3}")), 0644); err != nil {
|
||||
PE("cannot write build.go", err.Error())
|
||||
return
|
||||
}
|
||||
P("build:", old, "->", b)
|
||||
}
|
||||
|
||||
func runimports() { // ------------------------------------------------- update import blocks with goimports
|
||||
files, err := os.ReadDir(".")
|
||||
if err != nil {
|
||||
PE("cannot read directory", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
if f.IsDir() || !strings.HasSuffix(f.Name(), ".go") {
|
||||
continue
|
||||
}
|
||||
fileName := f.Name()
|
||||
|
||||
output, err := exec.Command(GOIMPORTS, fileName).Output()
|
||||
if err != nil {
|
||||
continue // does not compile yet - the build will report it
|
||||
}
|
||||
m := r6.FindStringSubmatch(string(output))
|
||||
if len(m) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
origContent, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
updatedContent := ReplaceFirst(r6, string(origContent), SF("import (%s)", m[1]))
|
||||
if string(origContent) == updatedContent {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := backup(fileName); err != nil {
|
||||
PE("backup of "+fileName+" failed", err.Error())
|
||||
continue
|
||||
}
|
||||
if err := os.WriteFile(fileName, []byte(updatedContent), 0644); err != nil {
|
||||
PE("cannot write "+fileName, err.Error())
|
||||
continue
|
||||
}
|
||||
PF("%s %s\n", Cw("imports updated:"), Cmb(fileName))
|
||||
}
|
||||
}
|
||||
|
||||
func buildall(name string) { // ------------------------------------------------------ build for all platforms
|
||||
targetversion, err := gettargetversion()
|
||||
if err != nil {
|
||||
PE(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
P(Ccb("build for all platforms"), Cwb(targetversion))
|
||||
if err := os.MkdirAll("bin", 0755); err != nil {
|
||||
PE("cannot create bin/", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
sums := []string{}
|
||||
failed := 0
|
||||
for _, osys := range oses {
|
||||
for _, arch := range arches {
|
||||
ofile := filepath.Join("bin", artifact(name, targetversion, osys, arch))
|
||||
|
||||
cmd := exec.Command(GO, "build", "-o", ofile, ".")
|
||||
cmd.Env = append(os.Environ(), "GOOS="+osys, "GOARCH="+arch)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
PE("build failed: "+ofile, strings.TrimSpace(string(out)))
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
|
||||
sum, err := sha256file(ofile)
|
||||
if err != nil {
|
||||
PE("cannot checksum "+ofile, err.Error())
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
sums = append(sums, SF("%s %s", sum, filepath.Base(ofile)))
|
||||
P(Cy(ofile), Cw(sum[:16]))
|
||||
}
|
||||
}
|
||||
|
||||
if failed > 0 { // do not publish a version whose binaries are incomplete
|
||||
PE(SF("%d of %d builds failed", failed, len(oses)*len(arches)), "version.txt not written")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filepath.Join("bin", "checksums.txt"), []byte(strings.Join(sums, "\n")+"\n"), 0644); err != nil {
|
||||
PE("cannot write bin/checksums.txt", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join("bin", "version.txt"), []byte(targetversion+"\n"), 0644); err != nil {
|
||||
PE("cannot write bin/version.txt", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
PO("all builds done", targetversion)
|
||||
}
|
||||
|
||||
func uploadall(name string) { // ------------------------------------------------------- upload builds to gozilla
|
||||
if !r7.MatchString(name) { // the name ends up in a remote shell command
|
||||
PE("invalid build name", name)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
targetversion, err := gettargetversion()
|
||||
if err != nil {
|
||||
PE(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
files := []string{}
|
||||
for _, osys := range oses {
|
||||
for _, arch := range arches {
|
||||
ofile := filepath.Join("bin", artifact(name, targetversion, osys, arch))
|
||||
if fileExists(ofile) {
|
||||
files = append(files, ofile)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(files) == 0 {
|
||||
PE("no builds found for version "+targetversion, "run 'gbld -a' first")
|
||||
os.Exit(1)
|
||||
}
|
||||
for _, f := range []string{"bin/checksums.txt", "bin/version.txt"} {
|
||||
if !fileExists(f) {
|
||||
PE("missing "+f, "run 'gbld -a' first")
|
||||
os.Exit(1)
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
|
||||
P(Ccb("upload builds to "+UPLOADHOST), Cwb(targetversion))
|
||||
dst := UPLOADPATH + "/" + name
|
||||
|
||||
if err := run("ssh", UPLOADHOST, SF("mkdir -p '%s' && rm -f '%s'/*_*_*", dst, dst)); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := run(append([]string{"scp"}, append(files, UPLOADHOST+":"+dst)...)...); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
PO("upload done", targetversion)
|
||||
}
|
||||
|
||||
func run(args ...string) error { // ------------------------------------------------- run a command, report failure
|
||||
P(Cy(strings.Join(args, " ")))
|
||||
out, err := exec.Command(args[0], args[1:]...).CombinedOutput()
|
||||
if err != nil {
|
||||
PE(args[0]+" failed", strings.TrimSpace(string(out)))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func artifact(name, version, osys, arch string) string { // ------------------------------ name of a build artifact
|
||||
return SF("%s_%s_%s_%s%s", name, version, osys, arch, exesuffix(osys))
|
||||
}
|
||||
|
||||
func exesuffix(osys string) string { // ------------------------------------------------ windows wants '.exe'
|
||||
if osys == "windows" {
|
||||
return ".exe"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func sha256file(path string) (string, error) { // --------------------------------------------- checksum of a file
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func watchtree(w *fsnotify.Watcher, root string) error { // ------------------------- watch a directory recursively
|
||||
return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil || !d.IsDir() {
|
||||
return nil //nolint - unreadable entries are simply not watched
|
||||
}
|
||||
base := filepath.Base(path)
|
||||
if path != root && (r1.MatchString(base) || slices.Contains(skipdirs, base)) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return w.Add(path)
|
||||
})
|
||||
}
|
||||
|
||||
func backup(src string) error { // ---------------------------------------------------------------- backup files
|
||||
if err := os.MkdirAll("tmp", 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sourceFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sourceFile.Close()
|
||||
|
||||
destFile, err := os.Create(SF("tmp/%s.%s", src, time.Now().Format("20060102150405")))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
if _, err := io.Copy(destFile, sourceFile); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := destFile.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prunebackups(src, KEEPBACKUPS)
|
||||
return nil
|
||||
}
|
||||
|
||||
func prunebackups(src string, keep int) { // ------------------------- keep tmp/ from growing with every rebuild
|
||||
old, err := filepath.Glob(SF("tmp/%s.*", src))
|
||||
if err != nil || len(old) <= keep {
|
||||
return
|
||||
}
|
||||
slices.Sort(old) // the timestamp suffix sorts chronologically
|
||||
for _, f := range old[:len(old)-keep] {
|
||||
os.Remove(f)
|
||||
}
|
||||
}
|
||||
|
||||
func gettargetversion() (string, error) { // --------------------------------------------- detect target version
|
||||
files, err := filepath.Glob("*.go")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
c, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if m := r5.FindSubmatch(c); m != nil {
|
||||
return string(m[1]), nil
|
||||
}
|
||||
}
|
||||
return "", SE(`target version not found, add 'var version = "x.y.z"' to your sources`)
|
||||
}
|
||||
|
||||
func catchsignals() { // ---------------------------------------------- stop the running build before we exit
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sig
|
||||
P()
|
||||
stopRunning()
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
|
||||
func stopRunning() { // ------------------------------------------------------------- stop running old process
|
||||
mu.Lock()
|
||||
p := running
|
||||
running = nil
|
||||
mu.Unlock()
|
||||
|
||||
if p == nil || p.cmd.Process == nil {
|
||||
return
|
||||
}
|
||||
pid := p.cmd.Process.Pid
|
||||
|
||||
select {
|
||||
case <-p.done: // already gone
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
p.cmd.Process.Signal(os.Interrupt)
|
||||
select {
|
||||
case <-p.done:
|
||||
P("process stopped:", pid)
|
||||
case <-time.After(1 * time.Second):
|
||||
p.cmd.Process.Kill()
|
||||
<-p.done
|
||||
P("process stopped forcefully:", pid)
|
||||
}
|
||||
}
|
||||
|
||||
func lookup(what string) string { // ------------------------------------------------- find a required program
|
||||
path, err := exec.LookPath(what)
|
||||
if err != nil {
|
||||
PE(what+" not found", "please install it and make sure it is in your PATH")
|
||||
os.Exit(1)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func fileExists(filename string) bool { // ------------------------------------------------ check if file exists
|
||||
info, err := os.Stat(filename)
|
||||
if os.IsNotExist(err) {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !info.IsDir()
|
||||
}
|
||||
|
||||
func dirName() string { // ------------------------------------------------------- get name of current directory
|
||||
pwd,err := os.Getwd()
|
||||
if err == nil {
|
||||
pwd, err := os.Getwd()
|
||||
if err == nil {
|
||||
return filepath.Base(pwd)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user