[mike@mwxm4]
This commit is contained in:
@@ -6,19 +6,20 @@
|
|||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
- 🔄 **Hot Reloader**: Automatically watches for file changes (via `fsnotify`) and rebuilds/restarts your application instantly.
|
- 🔄 **Hot Reloader**: Automatically watches for file changes (via `fsnotify`), recursively including subdirectories, and rebuilds/restarts your application instantly.
|
||||||
- 🔢 **Build Counter**: Increments a build number in `build.go` automatically with every compiled build.
|
- 🔢 **Build Counter**: Increments a build number in `build.go` automatically with every compiled build.
|
||||||
- 🧹 **Auto-Import & Formatting**: Runs `goimports` on save (optional via `-i`) to keep your imports and formatting clean.
|
- 🧹 **Auto-Import**: Runs `goimports` on save (optional via `-i`) to keep your imports clean — without touching the rest of your formatting.
|
||||||
- 🖥️ **Interactive Menu**: Uses a select menu (`gbld.menu.json`) if run without arguments, letting you define and run custom build tasks.
|
- 🖥️ **Interactive Menu**: Uses a select menu (`gbld.menu.json`) if run without arguments, letting you define and run custom build tasks.
|
||||||
- 🚀 **Cross-Compilation**: Easily builds binaries for multiple target operating systems (macOS, Linux, Windows) and architectures (amd64, arm64) using a single command.
|
- 🚀 **Cross-Compilation**: Easily builds binaries for multiple target operating systems (macOS, Linux, Windows) and architectures (amd64, arm64) using a single command.
|
||||||
- ☁️ **Remote Uploads**: Integrated command to securely upload build artifacts to a deployment server (`gozilla`).
|
- ☁️ **Remote Uploads**: Integrated command to upload build artifacts to a deployment server (`gozilla`).
|
||||||
|
- 🔐 **Verified Self-Update**: Downloads updates over HTTPS and refuses to install anything that does not match its published SHA-256 checksum.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
Make sure you have [Go](https://go.dev/) and `goimports` installed and available in your system path:
|
Make sure you have [Go](https://go.dev/) installed and available in your system path. `goimports` is only required if you use the `-i` flag:
|
||||||
```bash
|
```bash
|
||||||
go install golang.org/x/tools/cmd/goimports@latest
|
go install golang.org/x/tools/cmd/goimports@latest
|
||||||
```
|
```
|
||||||
@@ -39,15 +40,16 @@ go build -o /usr/local/bin/gbld
|
|||||||
gbld [flags] [target_name]
|
gbld [flags] [target_name]
|
||||||
```
|
```
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> If `target_name` is omitted, the name of the current directory is used. The tool expects a main source file matching that name (e.g., `target_name.go`).
|
> If `target_name` is omitted, the name of the current directory is used. The tool expects a main source file matching that name (e.g., `target_name.go`). The binary is always built from the package in the current directory, so the module path does not have to match the target name.
|
||||||
|
|
||||||
### CLI Flags
|
### CLI Flags
|
||||||
|
|
||||||
| Flag | Argument | Description |
|
| Flag | Argument | Description |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| `-b` | None | **Build-only** mode (compiles the binary but does not run it). |
|
| `-b` | None | **Build-only** mode (compiles the binary but does not run it). |
|
||||||
| `-1` | None | Run **once** (does not watch the directory for changes). |
|
| `-1` | None | Run **once** (does not watch the directory for changes), see [Exit Codes](#exit-codes). |
|
||||||
| `-i` | None | Runs `goimports` to clean up imports before building. |
|
| `-i` | None | Runs `goimports` to clean up imports before building. |
|
||||||
|
| `-x` | None | Exits after the first successful build and **leaves the started binary running**. |
|
||||||
| `-o` | `<args>` | Space-separated arguments to pass to the compiled binary on execution. |
|
| `-o` | `<args>` | Space-separated arguments to pass to the compiled binary on execution. |
|
||||||
| `-c` | `<command>` | Command to execute immediately after a successful build (before execution). |
|
| `-c` | `<command>` | Command to execute immediately after a successful build (before execution). |
|
||||||
| `-a` | None | Builds binaries for all supported platforms (Windows, macOS, Linux for AMD64/ARM64). |
|
| `-a` | None | Builds binaries for all supported platforms (Windows, macOS, Linux for AMD64/ARM64). |
|
||||||
@@ -55,11 +57,17 @@ gbld [flags] [target_name]
|
|||||||
| `-v` | None | Displays the current version of `gbld`. |
|
| `-v` | None | Displays the current version of `gbld`. |
|
||||||
| `-h` | None | Displays help and usage information. |
|
| `-h` | None | Displays help and usage information. |
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
| :--- | :--- |
|
||||||
|
| `GBLD_NO_UPDATE` | Set to any value to skip the update check at startup (useful offline or in CI). |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Configuration (`gbld.menu.json`)
|
## Configuration (`gbld.menu.json`)
|
||||||
|
|
||||||
To make running common tasks easier, you can place a `gbld.menu.json` file in your project directory. If you run `gbld` without any flags, it will parse this file and present an interactive selection menu:
|
To make running common tasks easier, you can place a `gbld.menu.json` file in your project directory. If you run `gbld` without any flags, it will parse this file and present an interactive selection menu (entries are shown in alphabetical order):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -74,16 +82,88 @@ To make running common tasks easier, you can place a `gbld.menu.json` file in yo
|
|||||||
## Under the Hood
|
## Under the Hood
|
||||||
|
|
||||||
### Build Increment
|
### Build Increment
|
||||||
When compiling, `gbld` parses `build.go` to find a pattern matching `var build = "<number>"`. It increments this number by 1 and updates the file automatically. This is useful for stamping build numbers/versions inside your binary.
|
When compiling, `gbld` parses `build.go` to find a pattern matching `var build = "<number>"`. It increments this number by 1 and updates the file automatically, leaving the rest of the line (including trailing comments) untouched. This is useful for stamping build numbers/versions inside your binary. If there is no `build.go`, this step is silently skipped.
|
||||||
|
|
||||||
|
### Target Version
|
||||||
|
`-a` and `-u` name their artifacts after the first `var version = "x.y.z"` found in the `*.go` files of the project. Ordinary builds do not need this variable.
|
||||||
|
|
||||||
### Automatic Backups
|
### Automatic Backups
|
||||||
Before `gbld` runs `goimports` on a file or modifies `build.go`, it creates a timestamped backup in a `./tmp/` directory inside the project workspace:
|
Before `gbld` runs `goimports` on a file or modifies `build.go`, it creates a timestamped backup in a `./tmp/` directory inside the project workspace (the directory is created on demand):
|
||||||
```
|
```
|
||||||
./tmp/gbld.go.20260625145800
|
./tmp/gbld.go.20260625145800
|
||||||
```
|
```
|
||||||
|
If a backup cannot be written, the file is **not** modified.
|
||||||
|
|
||||||
|
### File Watching
|
||||||
|
The project directory is watched recursively; directories created while `gbld` is running are picked up automatically. Hidden entries (`.git`, …) as well as `bin`, `tmp`, `vendor`, `node_modules` and `testdata` are skipped. Only writes and newly created `.go` files trigger a rebuild — a pure `touch` (mtime only) does not. Rebuilds are debounced by 100 ms and never run in parallel.
|
||||||
|
|
||||||
### Process Management
|
### Process Management
|
||||||
When hot-reloading, `gbld` handles process termination gracefully:
|
When hot-reloading, `gbld` handles process termination gracefully:
|
||||||
1. It sends an `os.Interrupt` (SIGINT) to the running binary.
|
1. It sends an `os.Interrupt` (SIGINT) to the running binary.
|
||||||
2. It waits up to 1 second for the process to exit clean.
|
2. It waits up to 1 second for the process to exit clean.
|
||||||
3. If it does not exit within the timeout, it terminates the process forcefully (`SIGKILL`).
|
3. If it does not exit within the timeout, it terminates the process forcefully (`SIGKILL`).
|
||||||
|
|
||||||
|
`gbld` itself also traps SIGINT/SIGTERM and stops the started binary before exiting, so no orphaned processes are left behind — except with `-x`, where detaching is the point.
|
||||||
|
|
||||||
|
### Exit Codes
|
||||||
|
With `-1`, `gbld` is usable from scripts and CI:
|
||||||
|
|
||||||
|
| Situation | Exit code |
|
||||||
|
| :--- | :--- |
|
||||||
|
| Build failed | `1` |
|
||||||
|
| `-b -1`, build succeeded | `0` |
|
||||||
|
| `-1` without `-b` | exit code of the built program (gbld waits for it) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-Compilation and Upload
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gbld -a # build bin/<name>_<version>_<os>_<arch> for all 6 targets
|
||||||
|
gbld -u # upload them to gozilla
|
||||||
|
```
|
||||||
|
|
||||||
|
`-a` writes Windows artifacts with an `.exe` suffix and additionally produces:
|
||||||
|
|
||||||
|
- `bin/checksums.txt` — SHA-256 of every artifact, in the format used by `shasum -a 256 -c`
|
||||||
|
- `bin/version.txt` — the published version
|
||||||
|
|
||||||
|
If any target fails to build, `version.txt` is **not** written and `gbld` exits with `1`, so a half-published version can never be announced. `-u` refuses build names containing anything but `A-Za-z0-9._-` and uploads the artifacts together with `checksums.txt` and `version.txt`.
|
||||||
|
|
||||||
|
### Self-Update
|
||||||
|
On startup `gbld` fetches `version.txt` from the update server (3 s timeout — an unreachable server never blocks a build). If a newer version is published, it asks before downloading, then verifies the download against the SHA-256 from `checksums.txt` before replacing itself. **If no matching checksum is published, the update is refused.** Publish new versions with `gbld -a` followed by `gbld -u`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Colors
|
||||||
|
|
||||||
|
Output uses the [Catppuccin Mocha](https://terminalcolors.com/themes/catppuccin/mocha/) palette as 24-bit color:
|
||||||
|
|
||||||
|
| Helper | Palette | Hex |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `Cr` / `Crb` | Red | `#f38ba8` |
|
||||||
|
| `Cg` / `Cgb` | Green | `#a6e3a1` |
|
||||||
|
| `Cy` / `Cyb` | Yellow | `#f9e2af` |
|
||||||
|
| `Cb` / `Cbb` | Blue | `#89b4fa` |
|
||||||
|
| `Cm` / `Cmb` | Pink | `#f5c2e7` |
|
||||||
|
| `Cc` / `Ccb` | Teal | `#94e2d5` |
|
||||||
|
| `Cw` | Subtext1 | `#bac2de` |
|
||||||
|
| `Cwb` | Text | `#cdd6f4` |
|
||||||
|
|
||||||
|
Mocha maps identical values to the normal and bright ANSI slots, so the bold helpers keep their hue and only add weight. The full palette is available as `CatMauve`, `CatPeach`, `CatLavender`, … in `tools.go`.
|
||||||
|
|
||||||
|
The interactive prompts (`survey`) are themed as well: their templates resolve colors through `{{color "green+hb"}}`-style names, and `tools.go` replaces that template function with one that maps those names onto the palette. This covers every prompt including its icons — question mark, select arrow, help and error markers — without copying any template.
|
||||||
|
|
||||||
|
Terminals that do not announce 24-bit color via `COLORTERM` fall back to the basic ANSI colors, so their own theme keeps deciding. `NO_COLOR` and non-TTY output disable colors entirely.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./... # unit tests (build counter, version detection, update verification, toolbox)
|
||||||
|
go vet ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> The sources are deliberately **not** `gofmt`-formatted (2-space indentation). The `-i` flag only replaces the `import (...)` block of a file and leaves the rest of the formatting alone — do not run `gofmt` over the tree.
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
var build = "362"
|
var build = "368"
|
||||||
|
|||||||
@@ -2,67 +2,73 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"crypto/sha256"
|
||||||
"bytes"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AlecAivazis/survey/v2"
|
"github.com/AlecAivazis/survey/v2"
|
||||||
"github.com/fsnotify/fsnotify"
|
"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 oses = []string{"darwin", "linux", "windows"}
|
||||||
var arches = []string{"arm64", "amd64"}
|
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 GO string
|
||||||
var GOIMPORTS string
|
var GOIMPORTS string
|
||||||
var xmd *exec.Cmd
|
|
||||||
|
|
||||||
var done chan bool
|
var mu sync.Mutex // guards 'running'
|
||||||
var r1 = regexp.MustCompile(`^\.`)
|
var running *proc
|
||||||
var r2 = regexp.MustCompile(`build\.go`)
|
|
||||||
var r3 = regexp.MustCompile(`\.go$`)
|
var r1 = regexp.MustCompile(`^\.`) // hidden file
|
||||||
var r4 = regexp.MustCompile(`var\s+build\s*=\s*"(.*)"$`)
|
var r2 = regexp.MustCompile(`^build\.go$`) // own build counter
|
||||||
var r5 = regexp.MustCompile(`(?i)var\s+version\s*=\s*"(.*)"$`)
|
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
|
func main() { // ========================================================================================== main
|
||||||
|
|
||||||
checkforupdate(UPDATEURL)
|
|
||||||
|
|
||||||
if len(os.Args) == 1 { // read predefined builds
|
if len(os.Args) == 1 { // ······································· no arguments: read predefined builds or default
|
||||||
c, err := os.ReadFile("gbld.menu.json")
|
os.Args = defaultargs()
|
||||||
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_b := flag.Bool("b", false, "")
|
||||||
@@ -81,19 +87,20 @@ func main() { // ===============================================================
|
|||||||
info()
|
info()
|
||||||
P()
|
P()
|
||||||
P(Cw("Usage:"))
|
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(Cy(" gbld [-a] [-u]"))
|
||||||
P(Cw(" -b build only"))
|
P(Cw(" -b build only"))
|
||||||
P(Cw(" -1 run only once"))
|
P(Cw(" -1 run only once, no watching (exit code: build error or exit code of the build)"))
|
||||||
P(Cw(" -i run gpimports before 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(" -o <OPTIONS> exec options"))
|
||||||
P(Cw(" -c <CMD> run command after build"))
|
P(Cw(" -c <CMD> run command after build"))
|
||||||
P(Cw(" -v show version"))
|
P(Cw(" -v show version"))
|
||||||
P(Cw(" -h show help"))
|
P(Cw(" -h show help"))
|
||||||
P(Cw(" -a build for all platforms"))
|
P(Cw(" -a build for all platforms"))
|
||||||
P(Cw(" -u upload builds to gozilla"))
|
P(Cw(" -u upload builds to " + UPLOADHOST))
|
||||||
P()
|
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()
|
||||||
P(Cw(" · if no option supplied try to read '") + Cy("gbld.menu.json") + Cw("'"))
|
P(Cw(" · if no option supplied try to read '") + Cy("gbld.menu.json") + Cw("'"))
|
||||||
P(Cm(` {`))
|
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(" · 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(Cw(" the current directory name with ") + Cy("-b -i -1") + Cw(" as options will be used"))
|
||||||
P()
|
P()
|
||||||
|
P(Cw(" · set ") + Cy("GBLD_NO_UPDATE=1") + Cw(" to skip the update check"))
|
||||||
|
P()
|
||||||
}
|
}
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
path, err := exec.LookPath("go") // ··························································· check for 'go'
|
o := opts{buildonly: *opt_b, once: *opt_1, imports: *opt_i, exitafter: *opt_x,
|
||||||
if err != nil {
|
postcmd: *opt_c, runargs: *opt_o}
|
||||||
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
|
if *opt_v { // ·············································································· show version
|
||||||
info()
|
info()
|
||||||
@@ -132,9 +130,18 @@ func main() { // ===============================================================
|
|||||||
os.Exit(0)
|
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
|
name := flag.Arg(0) // ································································ command line arguments
|
||||||
if len(name) == 0 {
|
if len(name) == 0 {
|
||||||
name=dirName()
|
name = dirName()
|
||||||
if len(name) == 0 {
|
if len(name) == 0 {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -143,82 +150,48 @@ func main() { // ===============================================================
|
|||||||
|
|
||||||
file := name + ".go"
|
file := name + ".go"
|
||||||
if !fileExists(file) {
|
if !fileExists(file) {
|
||||||
P(Crb("file not found: " + file))
|
PE("file not found", file)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!*opt_1 && !*opt_a && !*opt_u) {
|
|
||||||
PN("\033[2J\033[1;1H")
|
|
||||||
}
|
|
||||||
info()
|
|
||||||
|
|
||||||
if *opt_a { // ······························································· build for other platforms
|
if *opt_a { // ······························································· build for other platforms
|
||||||
targetversion:=gettargetversion()
|
info()
|
||||||
P(Ccb("build for all platforms"))
|
buildall(name)
|
||||||
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
|
if *opt_u { // ······································································ upload builds to gozilla
|
||||||
targetversion:=gettargetversion()
|
info()
|
||||||
P(Ccb("upload builds to gozilla"))
|
uploadall(name)
|
||||||
|
|
||||||
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) }
|
if *opt_a || *opt_u {
|
||||||
|
|
||||||
// Initial build
|
|
||||||
buildAndRun(name, file, *opt_b, *opt_1, *opt_i, *opt_c, *opt_o, *opt_x)
|
|
||||||
|
|
||||||
if *opt_1 {
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up fsnotify watcher
|
catchsignals() // ································· stop the started build when gbld itself is asked to quit
|
||||||
watcher, err := fsnotify.NewWatcher()
|
|
||||||
|
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 {
|
if err != nil {
|
||||||
PE(err.Error())
|
PE("cannot create watcher", err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer watcher.Close()
|
defer watcher.Close()
|
||||||
|
|
||||||
err = watcher.Add(".")
|
if err := watchtree(watcher, "."); err != nil {
|
||||||
if err != nil {
|
PE("cannot watch directory", err.Error())
|
||||||
PE(err.Error())
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
events := make(chan bool)
|
trigger := make(chan struct{}, 1)
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -226,221 +199,495 @@ func main() { // ===============================================================
|
|||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create {
|
if event.Op&fsnotify.Create == fsnotify.Create { // new directory: watch it as well
|
||||||
fileName := filepath.Base(event.Name)
|
if fi, err := os.Stat(event.Name); err == nil && fi.IsDir() {
|
||||||
if r3.MatchString(fileName) && !r1.MatchString(fileName) && !r2.MatchString(fileName) {
|
watchtree(watcher, event.Name)
|
||||||
events <- true
|
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:
|
case err, ok := <-watcher.Errors:
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
PE(err.Error())
|
PE("watcher", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var timer *time.Timer
|
for range trigger { // ································· debounce, then build serially - never in parallel
|
||||||
for {
|
for settled := false; !settled; {
|
||||||
select {
|
select {
|
||||||
case <-events:
|
case <-trigger:
|
||||||
if timer != nil {
|
case <-time.After(100 * time.Millisecond):
|
||||||
timer.Stop()
|
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) {
|
func defaultargs() []string { // ------------------------------------------------- arguments if none were supplied
|
||||||
PN("\033[2J\033[1;1H")
|
c, err := os.ReadFile("gbld.menu.json")
|
||||||
P(Cc("--- start compiling " + file + " ---"))
|
if err != nil {
|
||||||
|
return []string{"gbld", "-b", "-i", "-1", dirName()}
|
||||||
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)
|
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))
|
PF("%s %s\n", Cw("running:"), Cyb("go build "+name))
|
||||||
|
|
||||||
|
cmd := exec.Command(GO, "build", "-o", target, ".")
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
if err != nil { // ·········································································· build failed
|
if err != nil { // ·········································································· build failed
|
||||||
PN(Cy(string(out)))
|
PN(Cy(string(out)))
|
||||||
P(Cr("--- build failed: " + name + " ---"))
|
P(Cr("--- build failed: " + name + " ---"))
|
||||||
} else { // ·············································································· build succeeded
|
return false
|
||||||
P(Cb("--- compiling done: " + name + " ---"))
|
}
|
||||||
|
|
||||||
if len(opt_c) > 0 { // ··························································· execute post command
|
P(Cb("--- compiling done: " + name + " ---")) // ·········································· build succeeded
|
||||||
params := strings.Fields(opt_c)
|
|
||||||
app := params[0]
|
|
||||||
args := params[1:]
|
|
||||||
|
|
||||||
P(Cw(opt_c))
|
if len(o.postcmd) > 0 { // ······················································· execute post command
|
||||||
xmd = exec.Command(app, args...)
|
params := strings.Fields(o.postcmd)
|
||||||
xmd.Stdout = os.Stdout
|
P(Cw(o.postcmd))
|
||||||
xmd.Stdin = os.Stdin
|
post := exec.Command(params[0], params[1:]...)
|
||||||
xmd.Stderr = os.Stderr
|
post.Stdout = os.Stdout
|
||||||
xmd.Run()
|
post.Stdin = os.Stdin
|
||||||
}
|
post.Stderr = os.Stderr
|
||||||
|
if err := post.Run(); err != nil {
|
||||||
if !opt_b {
|
PE("post command failed", err.Error())
|
||||||
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 !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
|
func waitRunning() int { // ------------------------------------- wait for the started build, return its exit code
|
||||||
sourceFile,_ := os.Open(src)
|
mu.Lock()
|
||||||
defer sourceFile.Close()
|
p := running
|
||||||
dst:=SF("./tmp/%s.%s",src,time.Now().Format("20060102150405"))
|
running = nil
|
||||||
destFile,_ := os.Create(dst)
|
mu.Unlock()
|
||||||
defer destFile.Close()
|
|
||||||
io.Copy(destFile, sourceFile)
|
if p == nil {
|
||||||
destFile.Sync()
|
return 0
|
||||||
destFile.Close()
|
}
|
||||||
|
<-p.done
|
||||||
|
if p.cmd.ProcessState == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return p.cmd.ProcessState.ExitCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
func gettargetversion() string { // ------------------------------------------- detect target version
|
func bumpbuild() { // ------------------------------------------------------------------ increase build number
|
||||||
files, err := filepath.Glob("*.go")
|
c, err := os.ReadFile("build.go")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
PE(SF("Error listing files: %v", err))
|
return // no build.go, nothing to count
|
||||||
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 == "" {
|
m := r4.FindSubmatch(c)
|
||||||
PE("target version not found")
|
if m == nil {
|
||||||
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := cmd.Process.Signal(os.Interrupt)
|
old := string(m[2])
|
||||||
if err == nil {
|
b := Atoi(old) + 1
|
||||||
done := make(chan error, 1)
|
|
||||||
go func() { done <- cmd.Wait() }()
|
if err := backup("build.go"); err != nil { // never touch a file we could not save first
|
||||||
select {
|
PE("backup of build.go failed", err.Error())
|
||||||
case <-time.After(1 * time.Second):
|
return
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
func fileExists(filename string) bool { // ------------------------------------------------ check if file exists
|
||||||
info, err := os.Stat(filename)
|
info, err := os.Stat(filename)
|
||||||
if os.IsNotExist(err) {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return !info.IsDir()
|
return !info.IsDir()
|
||||||
}
|
}
|
||||||
|
|
||||||
func dirName() string { // ------------------------------------------------------- get name of current directory
|
func dirName() string { // ------------------------------------------------------- get name of current directory
|
||||||
pwd,err := os.Getwd()
|
pwd, err := os.Getwd()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return filepath.Base(pwd)
|
return filepath.Base(pwd)
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+235
@@ -0,0 +1,235 @@
|
|||||||
|
// ================================================================================== tests for gbld (mwx'2026)
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func inTempDir(t *testing.T) { // ------------------------------------- run a test in an empty working directory
|
||||||
|
t.Helper()
|
||||||
|
old, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
dir := t.TempDir()
|
||||||
|
if err := os.Chdir(dir); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { os.Chdir(old) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func write(t *testing.T, name, content string) { // ------------------------------------------ write a test file
|
||||||
|
t.Helper()
|
||||||
|
if err := os.WriteFile(name, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGettargetversion(t *testing.T) { // ------------------------------------------- find 'var version = ..'
|
||||||
|
inTempDir(t)
|
||||||
|
|
||||||
|
if _, err := gettargetversion(); err == nil {
|
||||||
|
t.Error("expected an error when no version is defined")
|
||||||
|
}
|
||||||
|
|
||||||
|
write(t, "other.go", "package main\n\n"+`var tbversion = "0.1.0"`+"\n")
|
||||||
|
if _, err := gettargetversion(); err == nil {
|
||||||
|
t.Error("tbversion must not be taken for the target version")
|
||||||
|
}
|
||||||
|
|
||||||
|
write(t, "app.go", "package main\n\n"+`var version = "2.3.0" // release`+"\n")
|
||||||
|
v, err := gettargetversion()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if v != "2.3.0" {
|
||||||
|
t.Errorf("got %q, want 2.3.0 (a trailing comment must not break the match)", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBumpbuild(t *testing.T) { // ------------------------------------- counter increases, comment survives
|
||||||
|
inTempDir(t)
|
||||||
|
|
||||||
|
write(t, "build.go", "package main\n\n"+`var build = "41" // counter`+"\n")
|
||||||
|
bumpbuild()
|
||||||
|
|
||||||
|
c, err := os.ReadFile("build.go")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := "package main\n\n" + `var build = "42" // counter` + "\n"
|
||||||
|
if string(c) != want {
|
||||||
|
t.Errorf("got %q, want %q", string(c), want)
|
||||||
|
}
|
||||||
|
|
||||||
|
files, _ := filepath.Glob("tmp/build.go.*") // the backup is what protects the users file
|
||||||
|
if len(files) != 1 {
|
||||||
|
t.Errorf("expected exactly one backup in tmp/, got %v", files)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackupPruning(t *testing.T) { // ---------------------------------- tmp/ must not grow without bounds
|
||||||
|
inTempDir(t)
|
||||||
|
|
||||||
|
write(t, "build.go", "package main\n\n"+`var build = "1"`+"\n")
|
||||||
|
for i := 0; i < KEEPBACKUPS+5; i++ { // same second: force distinct names to test the pruning itself
|
||||||
|
if err := backup("build.go"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
write(t, SF("tmp/build.go.2026010112000%02d", i), "old backup")
|
||||||
|
prunebackups("build.go", KEEPBACKUPS)
|
||||||
|
}
|
||||||
|
|
||||||
|
files, _ := filepath.Glob("tmp/build.go.*")
|
||||||
|
if len(files) > KEEPBACKUPS {
|
||||||
|
t.Errorf("got %d backups, want at most %d", len(files), KEEPBACKUPS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBumpbuildKeepsLongLines(t *testing.T) { // -------------- a >64k line must not truncate the file anymore
|
||||||
|
inTempDir(t)
|
||||||
|
|
||||||
|
long := strings.Repeat("x", 100*1024)
|
||||||
|
write(t, "build.go", "package main\n\n"+`var build = "1"`+"\n\nvar long = \""+long+"\"\n")
|
||||||
|
bumpbuild()
|
||||||
|
|
||||||
|
c, err := os.ReadFile("build.go")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(c), `var build = "2"`) {
|
||||||
|
t.Error("counter was not increased")
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(c), long) {
|
||||||
|
t.Error("long line was lost - the file got truncated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArtifact(t *testing.T) { // --------------------------------------------- windows artifacts need '.exe'
|
||||||
|
if got := artifact("gbld", "1.2.3", "linux", "amd64"); got != "gbld_1.2.3_linux_amd64" {
|
||||||
|
t.Errorf("got %q", got)
|
||||||
|
}
|
||||||
|
if got := artifact("gbld", "1.2.3", "windows", "amd64"); got != "gbld_1.2.3_windows_amd64.exe" {
|
||||||
|
t.Errorf("got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileExists(t *testing.T) { // --------------------------------- must not panic on unreadable entries
|
||||||
|
inTempDir(t)
|
||||||
|
|
||||||
|
write(t, "there.go", "package main\n")
|
||||||
|
if !fileExists("there.go") {
|
||||||
|
t.Error("existing file not found")
|
||||||
|
}
|
||||||
|
if fileExists("missing.go") {
|
||||||
|
t.Error("missing file reported as existing")
|
||||||
|
}
|
||||||
|
os.Mkdir("adir", 0755)
|
||||||
|
if fileExists("adir") {
|
||||||
|
t.Error("a directory is not a file")
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Mkdir("locked", 0000) // no permission: os.Stat fails with something other than IsNotExist
|
||||||
|
defer os.Chmod("locked", 0755)
|
||||||
|
if fileExists("locked/x.go") {
|
||||||
|
t.Error("unreadable path reported as existing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetchecksum(t *testing.T) { // ------------------------------------- checksums are read and validated
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/checksums.txt" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write([]byte("aabb gbld_1.0.0_linux_amd64\n" +
|
||||||
|
"0f0f gbld_1.0.0_windows_amd64.exe\n" +
|
||||||
|
"zzzz broken_sum\n"))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 5 * time.Second}
|
||||||
|
|
||||||
|
sum, err := getchecksum(client, srv.URL+"/checksums.txt", "gbld_1.0.0_linux_amd64")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(sum) != 2 || sum[0] != 0xaa || sum[1] != 0xbb {
|
||||||
|
t.Errorf("wrong checksum decoded: %x", sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := getchecksum(client, srv.URL+"/checksums.txt", "gbld_1.0.0_darwin_arm64"); err == nil {
|
||||||
|
t.Error("a missing entry must be an error - never update without a checksum")
|
||||||
|
}
|
||||||
|
if _, err := getchecksum(client, srv.URL+"/checksums.txt", "broken_sum"); err == nil {
|
||||||
|
t.Error("a malformed checksum must be an error")
|
||||||
|
}
|
||||||
|
if _, err := getchecksum(client, srv.URL+"/missing.txt", "gbld_1.0.0_linux_amd64"); err == nil {
|
||||||
|
t.Error("a missing checksums.txt must be an error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDoupdateRejectsWrongChecksum(t *testing.T) { // ------------- a manipulated download is never applied
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("this is not the binary you asked for"))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
err := doupdate(&http.Client{Timeout: 5 * time.Second}, srv.URL+"/gbld", []byte{0xde, 0xad, 0xbe, 0xef})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("update with a wrong checksum was accepted")
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.ToLower(err.Error()), "checksum") {
|
||||||
|
t.Errorf("expected a checksum error, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchStatus(t *testing.T) { // -------------------------------------- http errors must not be swallowed
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Error(w, "nope", http.StatusInternalServerError)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
if _, err := fetch(&http.Client{Timeout: 5 * time.Second}, srv.URL, 1<<20); err == nil {
|
||||||
|
t.Error("http 500 was reported as success")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToolbox(t *testing.T) { // ------------------------------------------------ toolbox edge cases
|
||||||
|
if got := Shortstr("abcdef", 4); got != "ab.." {
|
||||||
|
t.Errorf("Shortstr: got %q", got)
|
||||||
|
}
|
||||||
|
if got := Shortstr("abcdef", 1); got != ".." { // must not panic
|
||||||
|
t.Errorf("Shortstr with a tiny length: got %q", got)
|
||||||
|
}
|
||||||
|
if got := Dec("!!!not valid!!!"); got != "" { // must not panic
|
||||||
|
t.Errorf("Dec of garbage: got %q", got)
|
||||||
|
}
|
||||||
|
if got := Dec(Enc("secret")); got != "secret" {
|
||||||
|
t.Errorf("Enc/Dec roundtrip: got %q", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
in := []string{"a", "b", "a", "c"}
|
||||||
|
out := RemoveAllMatches(in, "a")
|
||||||
|
if strings.Join(out, "") != "bc" {
|
||||||
|
t.Errorf("RemoveAllMatches: got %v", out)
|
||||||
|
}
|
||||||
|
if strings.Join(in, "") != "abac" {
|
||||||
|
t.Errorf("RemoveAllMatches modified the callers slice: %v", in)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Checkip("10.0.0.0/8", "10.1.2.3") || Checkip("10.0.0.0/8", "192.168.0.1") {
|
||||||
|
t.Error("Checkip cidr")
|
||||||
|
}
|
||||||
|
if Checkip("10.0.0.0/8", "not an ip") {
|
||||||
|
t.Error("Checkip accepted an invalid address")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================================================= END
|
||||||
@@ -7,31 +7,30 @@ require (
|
|||||||
github.com/Masterminds/semver/v3 v3.5.0
|
github.com/Masterminds/semver/v3 v3.5.0
|
||||||
github.com/eknkc/basex v1.0.1
|
github.com/eknkc/basex v1.0.1
|
||||||
github.com/fatih/color v1.19.0
|
github.com/fatih/color v1.19.0
|
||||||
github.com/fsnotify/fsnotify v1.9.0
|
github.com/fsnotify/fsnotify v1.10.1
|
||||||
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
|
||||||
github.com/minio/selfupdate v0.6.0
|
github.com/minio/selfupdate v0.6.0
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
github.com/tidwall/gjson v1.19.0
|
github.com/tidwall/gjson v1.19.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
aead.dev/minisign v0.2.0 // indirect
|
aead.dev/minisign v0.3.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.2.0 // indirect
|
||||||
github.com/tidwall/pretty v1.2.0 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b // indirect
|
golang.org/x/crypto v0.54.0 // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
golang.org/x/term v0.45.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
golang.org/x/text v0.40.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk=
|
|
||||||
aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
|
aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
|
||||||
|
aead.dev/minisign v0.3.0 h1:8Xafzy5PEVZqYDNP60yJHARlW1eOQtsKNp/Ph2c0vRA=
|
||||||
|
aead.dev/minisign v0.3.0/go.mod h1:NLvG3Uoq3skkRMDuc3YHpWUTMTrSExqm+Ij73W13F6Y=
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||||
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
|
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
|
||||||
@@ -17,10 +18,10 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
|||||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
||||||
@@ -32,25 +33,24 @@ github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3x
|
|||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
|
||||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||||
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||||
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||||
github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU=
|
github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU=
|
||||||
github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM=
|
github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
|
||||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
@@ -67,18 +67,19 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
|
|||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
|
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
|
||||||
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
|
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
|
||||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
|
||||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b h1:QAqMVf3pSa6eeTsuklijukjXBlj7Es2QQplab+/RbQ4=
|
|
||||||
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||||
|
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
@@ -96,27 +97,25 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||||
|
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -18,71 +18,102 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AlecAivazis/survey/v2"
|
"github.com/AlecAivazis/survey/v2"
|
||||||
|
"github.com/AlecAivazis/survey/v2/core"
|
||||||
"github.com/AlecAivazis/survey/v2/terminal"
|
"github.com/AlecAivazis/survey/v2/terminal"
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/eknkc/basex"
|
"github.com/eknkc/basex"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
|
"github.com/mgutz/ansi"
|
||||||
"github.com/minio/selfupdate"
|
"github.com/minio/selfupdate"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
var tbversion = "0.5.0"
|
var tbversion = "0.7.0"
|
||||||
|
|
||||||
|
var HTTPTIMEOUT = 3 * time.Second // version.txt / checksums.txt - must never stall a build
|
||||||
|
var DOWNLOADTIMEOUT = 10 * time.Minute // the binary itself, http.Client.Timeout covers the whole body
|
||||||
|
|
||||||
var CHRS = "VW9IdGJ6eXh1T25DRHdrc2M5MlhOQVNQcEJFWnJhWVY2ZEowaFJLdmoxNUdxVDRJZkZpTTdRZW0zTFc4Z2w="
|
var CHRS = "VW9IdGJ6eXh1T25DRHdrc2M5MlhOQVNQcEJFWnJhWVY2ZEowaFJLdmoxNUdxVDRJZkZpTTdRZW0zTFc4Z2w="
|
||||||
var LR = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
var LR = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||||
|
|
||||||
func checkforupdate(URL string) { // ----------------------------------------------------- check for new version
|
func checkforupdate(URL string) { // ----------------------------------------------------- check for new version
|
||||||
prg := prgname()
|
prg := prgname()
|
||||||
resp, err := http.Get(URL+"/version.txt")
|
client := &http.Client{Timeout: HTTPTIMEOUT}
|
||||||
if err == nil {
|
|
||||||
scanner := bufio.NewScanner(resp.Body)
|
body, err := fetch(client, URL+"/version.txt", 1<<20) // update server unreachable: never block the build
|
||||||
if scanner.Scan() {
|
if err != nil { return }
|
||||||
lversion := strings.TrimSpace(scanner.Text())
|
|
||||||
sv_version, err := semver.NewVersion(version)
|
lversion := strings.TrimSpace(string(body)) // exactly as published - it names the artifact
|
||||||
if err == nil {
|
sv_version, err := semver.NewVersion(version)
|
||||||
sv_lversion, err := semver.NewVersion(lversion)
|
if err != nil { return }
|
||||||
if err == nil {
|
sv_lversion, err := semver.NewVersion(lversion)
|
||||||
if (sv_lversion.GreaterThan(sv_version)) {
|
if err != nil { return }
|
||||||
ans:=Yesno(SF("new '%s' version found (%s -> %s), update now?",
|
if !sv_lversion.GreaterThan(sv_version) { return }
|
||||||
prg,sv_version,sv_lversion),true,false);
|
|
||||||
if (ans) {
|
if !Yesno(SF("new '%s' version found (%s -> %s), update now?",prg,sv_version,lversion),true,false) {
|
||||||
updateurl:=SF("%s/%s_%s_%s_%s",URL,prg,lversion,runtime.GOOS,runtime.GOARCH)
|
return
|
||||||
|
|
||||||
if err := doupdate(updateurl); err != nil {
|
|
||||||
PO(SF("Update failed: %v\n", err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
PO("Update successful!","please run your last command again")
|
|
||||||
os.Exit(0)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target := SF("%s_%s_%s_%s",prg,lversion,runtime.GOOS,runtime.GOARCH)
|
||||||
|
if runtime.GOOS == "windows" { target += ".exe" }
|
||||||
|
|
||||||
|
sum, err := getchecksum(client, URL+"/checksums.txt", target) // no checksum -> no update
|
||||||
|
if err != nil {
|
||||||
|
PE("update aborted", err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := doupdate(&http.Client{Timeout: DOWNLOADTIMEOUT}, URL+"/"+target, sum); err != nil {
|
||||||
|
PE("update failed", err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
PO("update successful","please run your last command again")
|
||||||
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func doupdate(url string) error { // ----------------------------------------------------------------- do update
|
func doupdate(client *http.Client, url string, checksum []byte) error { // --------------------------- do update
|
||||||
resp, err := http.Get(url)
|
resp, err := client.Get(url)
|
||||||
if err != nil { return err }
|
if err != nil { return err }
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode != http.StatusOK { return fmt.Errorf("server returned status: %v", resp.Status) }
|
if resp.StatusCode != http.StatusOK { return SE("server returned status: %v", resp.Status) }
|
||||||
err = selfupdate.Apply(resp.Body, selfupdate.Options{})
|
return selfupdate.Apply(resp.Body, selfupdate.Options{Checksum: checksum})
|
||||||
if err != nil { return err }
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkaccess(NETS []string) { // ------------------------------------------------- check ip net based access
|
func getchecksum(client *http.Client, url string, name string) ([]byte, error) { // -- sha256 of a published file
|
||||||
|
body, err := fetch(client, url, 1<<20)
|
||||||
|
if err != nil { return nil, SE("no checksums published (%v)", err) }
|
||||||
|
|
||||||
|
for _, line := range strings.Split(string(body), "\n") {
|
||||||
|
f := strings.Fields(line)
|
||||||
|
if len(f) == 2 && f[1] == name {
|
||||||
|
sum, err := hex.DecodeString(f[0])
|
||||||
|
if err != nil { return nil, SE("bad checksum for %s", name) }
|
||||||
|
return sum, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, SE("no checksum for %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetch(client *http.Client, url string, max int64) ([]byte, error) { // ------------------- fetch a small file
|
||||||
|
resp, err := client.Get(url)
|
||||||
|
if err != nil { return nil, err }
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK { return nil, SE("server returned status: %v", resp.Status) }
|
||||||
|
return io.ReadAll(io.LimitReader(resp.Body, max))
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkaccess(NETS []string) { // ------------------------------------------------- check ip net based access
|
||||||
match:=0;
|
match:=0;
|
||||||
|
addrs, err := net.InterfaceAddrs()
|
||||||
|
if err != nil { PE("Error getting addresses", err.Error()); os.Exit(1) }
|
||||||
|
|
||||||
for _, validnet := range NETS {
|
for _, validnet := range NETS {
|
||||||
addrs, err := net.InterfaceAddrs()
|
|
||||||
if err != nil { PE("Error getting addresses"); os.Exit(1) }
|
|
||||||
_, ipNet, err := net.ParseCIDR(validnet)
|
_, ipNet, err := net.ParseCIDR(validnet)
|
||||||
|
if err != nil { PE("invalid network", validnet); continue }
|
||||||
for _, address := range addrs {
|
for _, address := range addrs {
|
||||||
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||||
if ipnet.IP.To4() != nil {
|
if ipnet.IP.To4() != nil {
|
||||||
@@ -102,8 +133,10 @@ func Enc(str string) string { // -----------------------------------------------
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Dec(str string) string { // ----------------------------------------------------------------- decode string
|
func Dec(str string) string { // ----------------------------------------------------------------- decode string
|
||||||
enc, _ := basex.NewEncoding(Db64(CHRS))
|
enc, err := basex.NewEncoding(Db64(CHRS))
|
||||||
b, _ := enc.Decode(str)
|
if err != nil { return "" }
|
||||||
|
b, err := enc.Decode(str)
|
||||||
|
if err != nil || len(b) < 2 { return "" }
|
||||||
return string(b[2:])
|
return string(b[2:])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +226,8 @@ func GETid(n int) string { // --------------------------------------------------
|
|||||||
|
|
||||||
func Mytable(rows *sql.Rows) []map[string]interface{} { // ----------------------------- load mysql result table
|
func Mytable(rows *sql.Rows) []map[string]interface{} { // ----------------------------- load mysql result table
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
columns, _ := rows.Columns()
|
columns, err := rows.Columns()
|
||||||
|
if err != nil { return nil }
|
||||||
count := len(columns)
|
count := len(columns)
|
||||||
tableData := make([]map[string]interface{}, 0)
|
tableData := make([]map[string]interface{}, 0)
|
||||||
values := make([]interface{}, count)
|
values := make([]interface{}, count)
|
||||||
@@ -202,7 +236,7 @@ func Mytable(rows *sql.Rows) []map[string]interface{} { // ---------------------
|
|||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
valuePtrs[i] = &values[i]
|
valuePtrs[i] = &values[i]
|
||||||
}
|
}
|
||||||
rows.Scan(valuePtrs...)
|
if err := rows.Scan(valuePtrs...); err != nil { break }
|
||||||
entry := make(map[string]interface{})
|
entry := make(map[string]interface{})
|
||||||
for i, col := range columns {
|
for i, col := range columns {
|
||||||
var v interface{}
|
var v interface{}
|
||||||
@@ -248,6 +282,7 @@ func Isflagpassed(name string) bool { // ---------------------------------------
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Body(r *http.Response) string { // ------------------------------------------------------------ http body
|
func Body(r *http.Response) string { // ------------------------------------------------------------ http body
|
||||||
|
defer r.Body.Close()
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return string(body)
|
return string(body)
|
||||||
@@ -281,14 +316,17 @@ func P(a ...any) (n int, err error) { return fmt.Fprintln(os.Std
|
|||||||
func PN(a ...any) (n int, err error) { return fmt.Fprint(os.Stdout, a...) }
|
func PN(a ...any) (n int, err error) { return fmt.Fprint(os.Stdout, a...) }
|
||||||
func PF(format string, a ...any) (n int, err error) { return fmt.Fprintf(os.Stdout, format, a...) }
|
func PF(format string, a ...any) (n int, err error) { return fmt.Fprintf(os.Stdout, format, a...) }
|
||||||
func SF(format string, a ...any) string { return fmt.Sprintf(format, a...) }
|
func SF(format string, a ...any) string { return fmt.Sprintf(format, a...) }
|
||||||
|
func SE(format string, a ...any) error { return fmt.Errorf(format, a...) }
|
||||||
|
|
||||||
func PE(msg ...string) (n int, err error) {
|
func PE(msg ...string) (n int, err error) {
|
||||||
if (len(msg)==2) { return fmt.Fprintf(os.Stdout, "%s: %s (%s)\n", Crb("ERROR"), Cwb(msg[0]),msg[1]) }
|
if (len(msg)==0) { return 0, nil }
|
||||||
|
if (len(msg)>=2) { return fmt.Fprintf(os.Stdout, "%s: %s (%s)\n", Crb("ERROR"), Cwb(msg[0]),msg[1]) }
|
||||||
return fmt.Fprintf(os.Stdout, "%s: %s\n", Crb("ERROR"), Cwb(msg[0]))
|
return fmt.Fprintf(os.Stdout, "%s: %s\n", Crb("ERROR"), Cwb(msg[0]))
|
||||||
}
|
}
|
||||||
func PO(msg ...string) (n int, err error) {
|
func PO(msg ...string) (n int, err error) {
|
||||||
if (len(msg)==2) { return fmt.Fprintf(os.Stdout, "%s: %s (%s)\n", Cgb("OK"), Cwb(msg[0]),msg[1]) }
|
if (len(msg)==0) { return 0, nil }
|
||||||
return fmt.Fprintf(os.Stdout, "%s: %s\n", Cgb("OK"), Cwb(msg[0]))
|
if (len(msg)>=2) { return fmt.Fprintf(os.Stdout, "%s: %s (%s)\n", Cgb("OK"), Cwb(msg[0]),msg[1]) }
|
||||||
|
return fmt.Fprintf(os.Stdout, "%s: %s\n", Cgb("OK"), Cwb(msg[0]))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------------ regular expression
|
// ------------------------------------------------------------------------------------------ regular expression
|
||||||
@@ -306,13 +344,16 @@ func Shortstr(s string, length int) string {
|
|||||||
if len(runes) <= length {
|
if len(runes) <= length {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
if length <= 2 {
|
||||||
|
return ".."
|
||||||
|
}
|
||||||
return string(runes[:length-2]) + ".."
|
return string(runes[:length-2]) + ".."
|
||||||
}
|
}
|
||||||
|
|
||||||
func SR(str string, n int) string { return strings.Repeat(str, n) }
|
func SR(str string, n int) string { return strings.Repeat(str, n) }
|
||||||
|
|
||||||
func RemoveAllMatches(slice []string, target string) []string { // remove matching string from array
|
func RemoveAllMatches(slice []string, target string) []string { // remove matching string from array
|
||||||
result := slice[:0]
|
result := make([]string, 0, len(slice)) // never modify the callers slice
|
||||||
for _, v := range slice {
|
for _, v := range slice {
|
||||||
if v != target {
|
if v != target {
|
||||||
result = append(result, v)
|
result = append(result, v)
|
||||||
@@ -330,7 +371,7 @@ func prgname() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
exename := filepath.Base(exepath)
|
exename := filepath.Base(exepath)
|
||||||
return exename
|
return strings.TrimSuffix(exename, ".exe") // windows: gbld.exe -> gbld
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------------ viper abbrevations
|
// ------------------------------------------------------------------------------------------ viper abbrevations
|
||||||
@@ -342,21 +383,118 @@ func VB(key string) bool { return viper.GetBool(key) }
|
|||||||
func VI(key string) int { return viper.GetInt(key) }
|
func VI(key string) int { return viper.GetInt(key) }
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------------------- text colors
|
// ------------------------------------------------------------------------------------------------- text colors
|
||||||
|
//
|
||||||
|
// catppuccin mocha - https://terminalcolors.com/themes/catppuccin/mocha/
|
||||||
|
// the palette is emitted as 24 bit color. terminals that do not announce truecolor via COLORTERM
|
||||||
|
// fall back to the basic ansi colors, so their own theme keeps deciding. NO_COLOR is honoured.
|
||||||
|
|
||||||
var Cr func(...interface{}) string = color.New(color.FgRed).SprintFunc()
|
type rgb struct{ r, g, b int }
|
||||||
var Cg func(...interface{}) string = color.New(color.FgGreen).SprintFunc()
|
|
||||||
var Cy func(...interface{}) string = color.New(color.FgYellow).SprintFunc()
|
|
||||||
var Cb func(...interface{}) string = color.New(color.FgBlue).SprintFunc()
|
|
||||||
var Cm func(...interface{}) string = color.New(color.FgMagenta).SprintFunc()
|
|
||||||
var Cc func(...interface{}) string = color.New(color.FgCyan).SprintFunc()
|
|
||||||
var Cw func(...interface{}) string = color.New(color.FgWhite).SprintFunc()
|
|
||||||
|
|
||||||
var Crb func(...interface{}) string = color.New(color.Bold, color.FgRed).SprintFunc()
|
var ( // ------------------------------------------------------------------------- catppuccin mocha palette
|
||||||
var Cgb func(...interface{}) string = color.New(color.Bold, color.FgGreen).SprintFunc()
|
CatRosewater = rgb{0xf5, 0xe0, 0xdc}
|
||||||
var Cyb func(...interface{}) string = color.New(color.Bold, color.FgYellow).SprintFunc()
|
CatFlamingo = rgb{0xf2, 0xcd, 0xcd}
|
||||||
var Cbb func(...interface{}) string = color.New(color.Bold, color.FgBlue).SprintFunc()
|
CatPink = rgb{0xf5, 0xc2, 0xe7}
|
||||||
var Cmb func(...interface{}) string = color.New(color.Bold, color.FgMagenta).SprintFunc()
|
CatMauve = rgb{0xcb, 0xa6, 0xf7}
|
||||||
var Ccb func(...interface{}) string = color.New(color.Bold, color.FgCyan).SprintFunc()
|
CatRed = rgb{0xf3, 0x8b, 0xa8}
|
||||||
var Cwb func(...interface{}) string = color.New(color.Bold, color.FgWhite).SprintFunc()
|
CatMaroon = rgb{0xeb, 0xa0, 0xac}
|
||||||
|
CatPeach = rgb{0xfa, 0xb3, 0x87}
|
||||||
|
CatYellow = rgb{0xf9, 0xe2, 0xaf}
|
||||||
|
CatGreen = rgb{0xa6, 0xe3, 0xa1}
|
||||||
|
CatTeal = rgb{0x94, 0xe2, 0xd5}
|
||||||
|
CatSky = rgb{0x89, 0xdc, 0xeb}
|
||||||
|
CatSapphire = rgb{0x74, 0xc7, 0xec}
|
||||||
|
CatBlue = rgb{0x89, 0xb4, 0xfa}
|
||||||
|
CatLavender = rgb{0xb4, 0xbe, 0xfe}
|
||||||
|
CatText = rgb{0xcd, 0xd6, 0xf4}
|
||||||
|
CatSubtext1 = rgb{0xba, 0xc2, 0xde}
|
||||||
|
CatSubtext0 = rgb{0xa6, 0xad, 0xc8}
|
||||||
|
CatOverlay1 = rgb{0x7f, 0x84, 0x9c}
|
||||||
|
CatSurface1 = rgb{0x45, 0x47, 0x5a}
|
||||||
|
)
|
||||||
|
|
||||||
|
var catansi = map[string]rgb{ // ------------------------------- how mocha fills the ansi slots (bright = normal)
|
||||||
|
"black": CatSurface1,
|
||||||
|
"red": CatRed,
|
||||||
|
"green": CatGreen,
|
||||||
|
"yellow": CatYellow,
|
||||||
|
"blue": CatBlue,
|
||||||
|
"magenta": CatPink,
|
||||||
|
"cyan": CatTeal,
|
||||||
|
"white": CatSubtext1,
|
||||||
|
"default": CatText,
|
||||||
|
}
|
||||||
|
|
||||||
|
func truecolor() bool { // ------------------------------------------ does the terminal understand 24 bit color
|
||||||
|
ct := strings.ToLower(os.Getenv("COLORTERM"))
|
||||||
|
return ct == "truecolor" || ct == "24bit"
|
||||||
|
}
|
||||||
|
|
||||||
|
func cfunc(c rgb, fallback color.Attribute, attrs ...color.Attribute) func(...interface{}) string {
|
||||||
|
col := color.New(attrs...)
|
||||||
|
if truecolor() {
|
||||||
|
col.AddRGB(c.r, c.g, c.b)
|
||||||
|
} else {
|
||||||
|
col.Add(fallback)
|
||||||
|
}
|
||||||
|
return col.SprintFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// mocha maps its ansi slots to red/green/yellow/blue/pink/teal, bright is identical to normal -
|
||||||
|
// so the bold variants keep the hue and only add weight, exactly as before.
|
||||||
|
|
||||||
|
var Cr func(...interface{}) string = cfunc(CatRed, color.FgRed)
|
||||||
|
var Cg func(...interface{}) string = cfunc(CatGreen, color.FgGreen)
|
||||||
|
var Cy func(...interface{}) string = cfunc(CatYellow, color.FgYellow)
|
||||||
|
var Cb func(...interface{}) string = cfunc(CatBlue, color.FgBlue)
|
||||||
|
var Cm func(...interface{}) string = cfunc(CatPink, color.FgMagenta)
|
||||||
|
var Cc func(...interface{}) string = cfunc(CatTeal, color.FgCyan)
|
||||||
|
var Cw func(...interface{}) string = cfunc(CatSubtext1, color.FgWhite)
|
||||||
|
|
||||||
|
var Crb func(...interface{}) string = cfunc(CatRed, color.FgRed, color.Bold)
|
||||||
|
var Cgb func(...interface{}) string = cfunc(CatGreen, color.FgGreen, color.Bold)
|
||||||
|
var Cyb func(...interface{}) string = cfunc(CatYellow, color.FgYellow, color.Bold)
|
||||||
|
var Cbb func(...interface{}) string = cfunc(CatBlue, color.FgBlue, color.Bold)
|
||||||
|
var Cmb func(...interface{}) string = cfunc(CatPink, color.FgMagenta, color.Bold)
|
||||||
|
var Ccb func(...interface{}) string = cfunc(CatTeal, color.FgCyan, color.Bold)
|
||||||
|
var Cwb func(...interface{}) string = cfunc(CatText, color.FgWhite, color.Bold) // text: the themes foreground
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------- survey prompt colors
|
||||||
|
//
|
||||||
|
// survey renders its prompts from templates that call {{color "green+hb"}} and friends, which normally
|
||||||
|
// ends up in mgutz/ansi - and that only speaks 16/256 colors. replacing the template function is enough
|
||||||
|
// to theme every prompt including its icons. survey computes its layout from a separate color free
|
||||||
|
// rendering, so the longer truecolor sequences cannot disturb the cursor placement.
|
||||||
|
|
||||||
|
func init() { core.TemplateFuncsWithColor["color"] = catcolor } // must run before the first prompt is parsed
|
||||||
|
|
||||||
|
func catcolor(style string) string { // ------------------------- mgutz/ansi style string -> catppuccin mocha
|
||||||
|
if style == "reset" {
|
||||||
|
return "\033[0m"
|
||||||
|
}
|
||||||
|
if !truecolor() || strings.Contains(style, ":") { // background colors stay with the original mapping
|
||||||
|
return ansi.ColorCode(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
name, attrs, _ := strings.Cut(style, "+")
|
||||||
|
c, ok := catansi[name]
|
||||||
|
if !ok {
|
||||||
|
return ansi.ColorCode(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
seq := ""
|
||||||
|
for _, a := range attrs { // 'h' (bright) is a no-op: mocha uses the same value for both
|
||||||
|
switch a {
|
||||||
|
case 'b':
|
||||||
|
seq += "1;"
|
||||||
|
case 'd':
|
||||||
|
seq += "2;"
|
||||||
|
case 'u':
|
||||||
|
seq += "4;"
|
||||||
|
case 'i':
|
||||||
|
seq += "7;"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SF("\033[%s38;2;%d;%d;%dm", seq, c.r, c.g, c.b)
|
||||||
|
}
|
||||||
|
|
||||||
// ========================================================================================================= END
|
// ========================================================================================================= END
|
||||||
Reference in New Issue
Block a user