[mike@mwxm4]
This commit is contained in:
@@ -17,3 +17,4 @@
|
|||||||
bin/*
|
bin/*
|
||||||
tmp/*
|
tmp/*
|
||||||
gbld
|
gbld
|
||||||
|
.mgshrc
|
||||||
|
|||||||
@@ -11,8 +11,7 @@
|
|||||||
- 🧹 **Auto-Import**: Runs `goimports` on save (optional via `-i`) to keep your imports clean — without touching the rest of your formatting.
|
- 🧹 **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 upload build artifacts to a deployment server (`gozilla`).
|
- 🔐 **Verified Self-Update**: Installs new versions from the Gitea releases over HTTPS and refuses anything that does not match its published SHA-256 checksum.
|
||||||
- 🔐 **Verified Self-Update**: Downloads updates over HTTPS and refuses to install anything that does not match its published SHA-256 checksum.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -53,15 +52,16 @@ gbld [flags] [target_name]
|
|||||||
| `-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). |
|
||||||
| `-u` | None | Uploads built binaries via SSH/SCP to the `gozilla` server. |
|
|
||||||
| `-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. |
|
||||||
|
| `--check-update` | None | Looks for a newer release and reports what it finds — installs nothing. |
|
||||||
|
| `--update` | None | Downloads the newest release and replaces the running binary with it. |
|
||||||
|
|
||||||
### Environment
|
### Environment
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
| :--- | :--- |
|
| :--- | :--- |
|
||||||
| `GBLD_NO_UPDATE` | Set to any value to skip the update check at startup (useful offline or in CI). |
|
| `GBLD_NO_UPDATE` | Set to any value to silence the background update check (useful offline or in CI). `--update` and `--check-update` still work. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ To make running common tasks easier, you can place a `gbld.menu.json` file in yo
|
|||||||
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.
|
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
|
### 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.
|
`-a` names its 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 (the directory is created on demand):
|
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):
|
||||||
@@ -116,22 +116,49 @@ With `-1`, `gbld` is usable from scripts and CI:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Cross-Compilation and Upload
|
## Cross-Compilation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gbld -a # build bin/<name>_<version>_<os>_<arch> for all 6 targets
|
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:
|
`-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/checksums.txt` — SHA-256 of every artifact, in the format used by `shasum -a 256 -c`
|
||||||
- `bin/version.txt` — the published version
|
- `bin/version.txt` — the version that was built
|
||||||
|
|
||||||
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`.
|
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.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> `-a` is the path for the programs `gbld` builds. `gbld` itself is released with `build.sh` (see below), which uses a different naming — both write `bin/checksums.txt`, so whichever ran last is the one that file describes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Releasing gbld itself
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build.sh # steps the patch version, builds every platform
|
||||||
|
VERSION=1.22.0 ./build.sh # a minor or major step is named outright
|
||||||
|
PLATFORMS="linux/amd64" ./build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
`build.sh` writes into `bin/` exactly what a release needs:
|
||||||
|
|
||||||
|
- `gbld-<goos>-<goarch>` — the binaries, `.exe` for Windows, built with `-trimpath -s -w` and the version injected via `-ldflags`
|
||||||
|
- `checksums.txt` — their SHA-256, in the format of `shasum -a 256`
|
||||||
|
|
||||||
|
`version.txt` holds the version just built and is stepped by `0.0.1` on every run. Upload **all** of these files to a Gitea release whose tag is the bare version number (`1.21.1`, a leading `v` is allowed) — that is the layout `--update` expects.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> `gbld -a` names its artifacts from `var version` in the sources, `build.sh` from `version.txt`, and nothing writes to a `.go` file. For gbld's own releases `build.sh` is the way — `var version` only shows up in a bare `go build`.
|
||||||
|
|
||||||
### Self-Update
|
### 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`.
|
|
||||||
|
`selfupdate.go` holds the whole mechanism and is meant to be copied into other programs: adjust the block at its head, hang the two options into the flags, done. It needs nothing but the standard library.
|
||||||
|
|
||||||
|
Once a day, in the background, `gbld` asks the Gitea for the newest release and notes the answer in the cache directory. The next run reads that note and prints one line if something newer exists — the foreground never touches the network, so an unreachable server can never delay a build. The hint appears only on a terminal: in a pipe, a script or under cron, `gbld` stays quiet, as it does with `GBLD_NO_UPDATE` set.
|
||||||
|
|
||||||
|
`--update` fetches the asset for the running platform, weighs it against the SHA-256 from the release's `checksums.txt`, and only then replaces the running binary — after calling the fresh one once with `-v` to be sure it runs at all. **A release without a matching checksum is refused**, and a download that does not match is deleted, not installed. The certificate of the Gitea is verified like any other.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
var build = "368"
|
var build = "369"
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Build gbld for the usual platforms into ./bin, auto-incrementing the patch
|
||||||
|
# version by 0.0.1 on every build.
|
||||||
|
#
|
||||||
|
# version.txt holds the currently built version. Each run increments the patch
|
||||||
|
# component, then builds every platform with that one version injected via
|
||||||
|
# -ldflags, and writes it back. So version.txt always reflects the version of
|
||||||
|
# the binaries just built, and all of them carry the same one.
|
||||||
|
#
|
||||||
|
# What ends up in ./bin is exactly what a release wants:
|
||||||
|
#
|
||||||
|
# gbld-<goos>-<goarch> the binaries, '.exe' for windows
|
||||||
|
# checksums.txt their sha256, in the format of `shasum -a 256`
|
||||||
|
#
|
||||||
|
# selfupdate.go looks for both. Upload all of them to a Gitea release whose tag
|
||||||
|
# is the bare version number, and --update finds them; without checksums.txt it
|
||||||
|
# refuses to install anything, so do not leave it behind.
|
||||||
|
#
|
||||||
|
# This is not the naming of gbld's own -a, which builds
|
||||||
|
# "<name>_<version>_<os>_<arch>" — that path is for the programs gbld builds,
|
||||||
|
# and it reads 'var version' out of the sources rather than version.txt.
|
||||||
|
# Both write bin/checksums.txt, each for its own set of names.
|
||||||
|
#
|
||||||
|
# Override the platform list to build just one:
|
||||||
|
# PLATFORMS="linux/amd64" ./build.sh
|
||||||
|
#
|
||||||
|
# A plain run only ever steps the patch. A minor or major step is taken by
|
||||||
|
# naming the version outright, which is then written back like any other:
|
||||||
|
# VERSION=1.22.0 ./build.sh
|
||||||
|
#
|
||||||
|
# build.go carries a separate build counter that shows up in -v next to the
|
||||||
|
# version; nothing here touches it — gbld itself steps that one on every
|
||||||
|
# compile it does.
|
||||||
|
#
|
||||||
|
# -s -w drops the symbol table and DWARF info, -trimpath keeps build paths out
|
||||||
|
# of the binary; together they roughly halve it. Neither affects a panic trace.
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
PLATFORMS=${PLATFORMS:-"darwin/arm64 darwin/amd64 linux/amd64 linux/arm64 windows/amd64 windows/arm64"}
|
||||||
|
|
||||||
|
if [ -n "$VERSION" ]; then
|
||||||
|
NV="$VERSION"
|
||||||
|
else
|
||||||
|
V=$(cat version.txt 2>/dev/null || echo 1.21.0)
|
||||||
|
|
||||||
|
# split MAJOR.MINOR.PATCH and increment PATCH (no carry: 1.21.9 -> 1.21.10)
|
||||||
|
MAJOR=${V%%.*}
|
||||||
|
REST=${V#*.}
|
||||||
|
MINOR=${REST%%.*}
|
||||||
|
PATCH=${REST#*.}
|
||||||
|
PATCH=$((PATCH + 1))
|
||||||
|
NV="$MAJOR.$MINOR.$PATCH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Linux brings sha256sum, macOS brings shasum; both write "<hash> <name>".
|
||||||
|
sha256() {
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then
|
||||||
|
sha256sum "$@"
|
||||||
|
else
|
||||||
|
shasum -a 256 "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir -p bin
|
||||||
|
HOST="$(go env GOOS)/$(go env GOARCH)"
|
||||||
|
BUILT=""
|
||||||
|
|
||||||
|
for p in $PLATFORMS; do
|
||||||
|
os=${p%/*}
|
||||||
|
arch=${p#*/}
|
||||||
|
name="gbld-$os-$arch"
|
||||||
|
[ "$os" = "windows" ] && name="$name.exe"
|
||||||
|
|
||||||
|
# CGO_ENABLED=0 throughout: it makes the cross builds work without a
|
||||||
|
# toolchain per target and the binaries static. gbld only needs the
|
||||||
|
# filesystem, the network and a shell, so nothing is lost by dropping cgo.
|
||||||
|
CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" \
|
||||||
|
go build -trimpath -ldflags "-s -w -X main.version=$NV" -o "bin/$name" .
|
||||||
|
|
||||||
|
BUILT="$BUILT $name"
|
||||||
|
|
||||||
|
if [ "$p" = "$HOST" ]; then
|
||||||
|
ln -sf "$name" bin/gbld # the one for this machine
|
||||||
|
echo " bin/$name -> bin/gbld"
|
||||||
|
else
|
||||||
|
echo " bin/$name"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Only what this run built: a checksum for a file from an earlier version would
|
||||||
|
# be a checksum for a binary nobody is publishing.
|
||||||
|
(cd bin && sha256 $BUILT > checksums.txt)
|
||||||
|
echo " bin/checksums.txt"
|
||||||
|
|
||||||
|
echo "$NV" > version.txt
|
||||||
|
echo "built gbld v$NV"
|
||||||
@@ -24,11 +24,12 @@ import (
|
|||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.21.0"
|
// version is a var, not a const, so build.sh can put the built number in via
|
||||||
|
// -ldflags "-X main.version=…". The value here only ever shows up in a bare
|
||||||
var UPDATEURL = "https://gozilla.fhi.mpg.de/gbld"
|
// `go build`; the version a release carries is the one in version.txt. Note
|
||||||
var UPLOADHOST = "root@gozilla"
|
// that -a below names its artifacts from this line and not from version.txt —
|
||||||
var UPLOADPATH = "/db/www"
|
// for gbld's own releases build.sh is the way, not -a.
|
||||||
|
var version = "1.22.0"
|
||||||
|
|
||||||
var oses = []string{"darwin", "linux", "windows"}
|
var oses = []string{"darwin", "linux", "windows"}
|
||||||
var arches = []string{"arm64", "amd64"}
|
var arches = []string{"arm64", "amd64"}
|
||||||
@@ -49,7 +50,6 @@ var r3 = regexp.MustCompile(`\.go$`) // go sour
|
|||||||
var r4 = regexp.MustCompile(`(?m)^(\s*var\s+build\s*=\s*")([^"]*)(")`) // build counter
|
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 r5 = regexp.MustCompile(`(?im)^\s*var\s+version\s*=\s*"([^"]*)"`) // target version
|
||||||
var r6 = regexp.MustCompile(`(?s)import\s*\((.*?)\)`) // import block
|
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
|
type opts struct { // ------------------------------------------------------------------------- command line options
|
||||||
buildonly bool
|
buildonly bool
|
||||||
@@ -80,7 +80,11 @@ func main() { // ===============================================================
|
|||||||
opt_a := flag.Bool("a", false, "")
|
opt_a := flag.Bool("a", false, "")
|
||||||
opt_v := flag.Bool("v", false, "")
|
opt_v := flag.Bool("v", false, "")
|
||||||
opt_h := flag.Bool("h", false, "")
|
opt_h := flag.Bool("h", false, "")
|
||||||
opt_u := flag.Bool("u", false, "")
|
|
||||||
|
opt_update := flag.Bool("update", false, "") // selfupdate.go
|
||||||
|
opt_checkupdate := flag.Bool("check-update", false, "")
|
||||||
|
opt_refresh := flag.Bool(strings.TrimPrefix(updateRefreshFlag, "--"), false, "") // background run, not in the
|
||||||
|
// help, name tied to the const
|
||||||
|
|
||||||
flag.Usage = func() {
|
flag.Usage = func() {
|
||||||
P()
|
P()
|
||||||
@@ -88,7 +92,7 @@ func main() { // ===============================================================
|
|||||||
P()
|
P()
|
||||||
P(Cw("Usage:"))
|
P(Cw("Usage:"))
|
||||||
P(Cy(" gbld [-b] [-1] [-i] [-x] [-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]"))
|
||||||
P(Cw(" -b build only"))
|
P(Cw(" -b build only"))
|
||||||
P(Cw(" -1 run only once, no watching (exit code: build error or exit code of the build)"))
|
P(Cw(" -1 run only once, no watching (exit code: build error or exit code of the build)"))
|
||||||
P(Cw(" -i run goimports before build"))
|
P(Cw(" -i run goimports before build"))
|
||||||
@@ -98,7 +102,8 @@ func main() { // ===============================================================
|
|||||||
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 " + UPLOADHOST))
|
P(Cw(" --check-update look for a newer release"))
|
||||||
|
P(Cw(" --update download and install the newest release"))
|
||||||
P()
|
P()
|
||||||
P(Cw(" · if no build name 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()
|
||||||
@@ -120,18 +125,14 @@ func main() { // ===============================================================
|
|||||||
o := opts{buildonly: *opt_b, once: *opt_1, imports: *opt_i, exitafter: *opt_x,
|
o := opts{buildonly: *opt_b, once: *opt_1, imports: *opt_i, exitafter: *opt_x,
|
||||||
postcmd: *opt_c, runargs: *opt_o}
|
postcmd: *opt_c, runargs: *opt_o}
|
||||||
|
|
||||||
if *opt_v { // ·············································································· show version
|
// These run before the toolchain is looked for: none of them needs 'go' or // no go,
|
||||||
info()
|
// 'goimports', and selfupdate.go probes a fresh download with 'gbld -v', // no goimports
|
||||||
os.Exit(0)
|
// which therefore has to work on a machine that has neither.
|
||||||
}
|
if (*opt_refresh) { selfUpdate.refresh(); return // update options
|
||||||
|
} else if (*opt_update) { runupdate(selfUpdate.install); return
|
||||||
if *opt_h { // ····················································································· show help
|
} else if (*opt_checkupdate) { runupdate(selfUpdate.check); return
|
||||||
flag.Usage()
|
} else if (*opt_v) { info(); return
|
||||||
os.Exit(0)
|
} else if (*opt_h) { flag.Usage(); return
|
||||||
}
|
|
||||||
|
|
||||||
if os.Getenv("GBLD_NO_UPDATE") == "" { // ······································· check for a newer gbld version
|
|
||||||
checkforupdate(UPDATEURL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GO = lookup("go") // ······················································· check for the required toolchain
|
GO = lookup("go") // ······················································· check for the required toolchain
|
||||||
@@ -157,14 +158,6 @@ func main() { // ===============================================================
|
|||||||
if *opt_a { // ······························································· build for other platforms
|
if *opt_a { // ······························································· build for other platforms
|
||||||
info()
|
info()
|
||||||
buildall(name)
|
buildall(name)
|
||||||
}
|
|
||||||
|
|
||||||
if *opt_u { // ······································································ upload builds to gozilla
|
|
||||||
info()
|
|
||||||
uploadall(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *opt_a || *opt_u {
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +267,14 @@ func buildAndRun(name, file string, o opts) bool { // -------------------------
|
|||||||
PN("\033[2J\033[1;1H")
|
PN("\033[2J\033[1;1H")
|
||||||
}
|
}
|
||||||
info()
|
info()
|
||||||
|
|
||||||
|
// Costs nothing: the hint comes from the note in the cache, the asking
|
||||||
|
// happens once a day at most and in the background. It stands after the
|
||||||
|
// screen is cleared, or every rebuild would wipe it away again.
|
||||||
|
if hint := selfUpdate.daily(); hint != "" {
|
||||||
|
P(Cy(hint))
|
||||||
|
}
|
||||||
|
|
||||||
P(Cc("--- start compiling " + file + " ---"))
|
P(Cc("--- start compiling " + file + " ---"))
|
||||||
|
|
||||||
stopRunning()
|
stopRunning()
|
||||||
@@ -478,58 +479,11 @@ func buildall(name string) { // ------------------------------------------------
|
|||||||
PO("all builds done", targetversion)
|
PO("all builds done", targetversion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func uploadall(name string) { // ------------------------------------------------------- upload builds to gozilla
|
func runupdate(task func(io.Writer) error) { // ----------------------------------- run a task from selfupdate.go
|
||||||
if !r7.MatchString(name) { // the name ends up in a remote shell command
|
if err := task(os.Stdout); err != nil {
|
||||||
PE("invalid build name", name)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
targetversion, err := gettargetversion()
|
|
||||||
if err != nil {
|
|
||||||
PE(err.Error())
|
PE(err.Error())
|
||||||
os.Exit(1)
|
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
|
func artifact(name, version, osys, arch string) string { // ------------------------------ name of a build artifact
|
||||||
|
|||||||
+89
-25
@@ -2,6 +2,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
@@ -142,65 +145,126 @@ func TestFileExists(t *testing.T) { // --------------------------------- must no
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetchecksum(t *testing.T) { // ------------------------------------- checksums are read and validated
|
func TestUpdateFindSum(t *testing.T) { // ----------------------------------- checksums are read and validated
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
good := strings.Repeat("ab", 32) // 32 bytes, as a sha256 has
|
||||||
if r.URL.Path != "/checksums.txt" {
|
body := []byte(good + " gbld-linux-amd64\n" +
|
||||||
http.NotFound(w, r)
|
good + " *gbld-windows-amd64.exe\n" + // binary mode marks the name with '*'
|
||||||
return
|
"zzzz broken_sum\n")
|
||||||
}
|
|
||||||
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 := updateFindSum(body, "gbld-linux-amd64")
|
||||||
|
|
||||||
sum, err := getchecksum(client, srv.URL+"/checksums.txt", "gbld_1.0.0_linux_amd64")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if len(sum) != 2 || sum[0] != 0xaa || sum[1] != 0xbb {
|
if hex.EncodeToString(sum) != good {
|
||||||
t.Errorf("wrong checksum decoded: %x", sum)
|
t.Errorf("wrong checksum decoded: %x", sum)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := getchecksum(client, srv.URL+"/checksums.txt", "gbld_1.0.0_darwin_arm64"); err == nil {
|
if _, err := updateFindSum(body, "gbld-windows-amd64.exe"); err != nil {
|
||||||
|
t.Errorf("the '*' of binary mode belongs to the format, not to the name: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := updateFindSum(body, "gbld-darwin-arm64"); err == nil {
|
||||||
t.Error("a missing entry must be an error - never update without a checksum")
|
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 {
|
if _, err := updateFindSum(body, "broken_sum"); err == nil {
|
||||||
t.Error("a malformed checksum must be an error")
|
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 {
|
if _, err := updateFindSum([]byte(strings.Repeat("ab", 16)+" gbld-linux-amd64\n"), "gbld-linux-amd64"); err == nil {
|
||||||
t.Error("a missing checksums.txt must be an error")
|
t.Error("a hash of the wrong length must be an error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDoupdateRejectsWrongChecksum(t *testing.T) { // ------------- a manipulated download is never applied
|
func TestUpdateChecksum(t *testing.T) { // ------------------------- the checksum comes out of the release itself
|
||||||
|
want := strings.Repeat("cd", 32)
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write([]byte("this is not the binary you asked for"))
|
w.Write([]byte(want + " gbld-linux-amd64\n"))
|
||||||
}))
|
}))
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
err := doupdate(&http.Client{Timeout: 5 * time.Second}, srv.URL+"/gbld", []byte{0xde, 0xad, 0xbe, 0xef})
|
rel := updateRelease{TagName: "v1.2.3", Assets: []updateAsset{
|
||||||
|
{Name: "gbld-linux-amd64", URL: srv.URL + "/gbld-linux-amd64"},
|
||||||
|
{Name: updateChecksums, URL: srv.URL + "/" + updateChecksums},
|
||||||
|
}}
|
||||||
|
|
||||||
|
sum, err := selfUpdate.checksum(rel, "gbld-linux-amd64")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if hex.EncodeToString(sum) != want {
|
||||||
|
t.Errorf("wrong checksum: %x", sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
bare := updateRelease{TagName: "v1.2.3", Assets: rel.Assets[:1]} // no checksums.txt published
|
||||||
|
if _, err := selfUpdate.checksum(bare, "gbld-linux-amd64"); err == nil {
|
||||||
|
t.Error("a release without checksums must be refused")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateDownloadRejectsWrongChecksum(t *testing.T) { // ------ a manipulated download is never installed
|
||||||
|
body := []byte("this is not the binary you asked for")
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(body)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
exe := filepath.Join(dir, "gbld")
|
||||||
|
a := &updateAsset{Name: "gbld-linux-amd64", Size: int64(len(body)), URL: srv.URL + "/gbld-linux-amd64"}
|
||||||
|
|
||||||
|
_, err := selfUpdate.download(a, exe, 0755, []byte{0xde, 0xad, 0xbe, 0xef})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("update with a wrong checksum was accepted")
|
t.Fatal("a download with a wrong checksum was accepted")
|
||||||
}
|
}
|
||||||
if !strings.Contains(strings.ToLower(err.Error()), "checksum") {
|
if !strings.Contains(strings.ToLower(err.Error()), "checksum") {
|
||||||
t.Errorf("expected a checksum error, got: %v", err)
|
t.Errorf("expected a checksum error, got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
left, _ := filepath.Glob(filepath.Join(dir, "*"))
|
||||||
|
if len(left) != 0 {
|
||||||
|
t.Errorf("the rejected download was left behind: %v", left)
|
||||||
|
}
|
||||||
|
|
||||||
|
sum := sha256.Sum256(body) // the same bytes, now with the checksum that fits
|
||||||
|
tmp, err := selfUpdate.download(a, exe, 0755, sum[:])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("a matching checksum was refused: %v", err)
|
||||||
|
}
|
||||||
|
os.Remove(tmp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetchStatus(t *testing.T) { // -------------------------------------- http errors must not be swallowed
|
func TestUpdateGetStatus(t *testing.T) { // ---------------------------------- http errors must not be swallowed
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Error(w, "nope", http.StatusInternalServerError)
|
http.Error(w, "nope", http.StatusInternalServerError)
|
||||||
}))
|
}))
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
if _, err := fetch(&http.Client{Timeout: 5 * time.Second}, srv.URL, 1<<20); err == nil {
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if _, err := updateGet(ctx, srv.URL); err == nil {
|
||||||
t.Error("http 500 was reported as success")
|
t.Error("http 500 was reported as success")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUpdateCompare(t *testing.T) { // ------------------- versions are numbers, not strings: 1.21.10 > 1.21.9
|
||||||
|
for _, c := range []struct {
|
||||||
|
a, b string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"1.21.10", "1.21.9", 1},
|
||||||
|
{"v1.21.0", "1.21.0", 0},
|
||||||
|
{"1.21", "1.21.0", 0},
|
||||||
|
{"1.21.0-rc1", "1.21.0", -1}, // not finished yet
|
||||||
|
{"1.9.0", "1.10.0", -1},
|
||||||
|
} {
|
||||||
|
if got := updateCompare(c.a, c.b); got != c.want {
|
||||||
|
t.Errorf("updateCompare(%q,%q) = %d, want %d", c.a, c.b, got, c.want)
|
||||||
|
}
|
||||||
|
if got := updateCompare(c.b, c.a); got != -c.want {
|
||||||
|
t.Errorf("updateCompare(%q,%q) = %d, want %d", c.b, c.a, got, -c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestToolbox(t *testing.T) { // ------------------------------------------------ toolbox edge cases
|
func TestToolbox(t *testing.T) { // ------------------------------------------------ toolbox edge cases
|
||||||
if got := Shortstr("abcdef", 4); got != "ab.." {
|
if got := Shortstr("abcdef", 4); got != "ab.." {
|
||||||
t.Errorf("Shortstr: got %q", got)
|
t.Errorf("Shortstr: got %q", got)
|
||||||
|
|||||||
@@ -4,18 +4,15 @@ go 1.26.1
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||||
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.10.1
|
github.com/fsnotify/fsnotify v1.10.1
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
|
||||||
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.3.0 // indirect
|
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.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.15 // indirect
|
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||||
@@ -29,7 +26,6 @@ require (
|
|||||||
github.com/tidwall/match v1.2.0 // indirect
|
github.com/tidwall/match v1.2.0 // indirect
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||||
golang.org/x/crypto v0.54.0 // indirect
|
|
||||||
golang.org/x/sys v0.47.0 // indirect
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
golang.org/x/term v0.45.0 // indirect
|
golang.org/x/term v0.45.0 // indirect
|
||||||
golang.org/x/text v0.40.0 // indirect
|
golang.org/x/text v0.40.0 // indirect
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
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/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
|
||||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
|
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
|
||||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
||||||
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
|
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
|
||||||
@@ -41,8 +36,6 @@ github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI
|
|||||||
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 h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
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/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
|
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
|
||||||
github.com/pelletier/go-toml/v2 v2.4.3/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=
|
||||||
@@ -75,38 +68,27 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
|
|||||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
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-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/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-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=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
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.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.47.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-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/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 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
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.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.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||||
|
|||||||
+585
@@ -0,0 +1,585 @@
|
|||||||
|
// selfupdate.go — updating oneself from the releases of a Gitea instance.
|
||||||
|
//
|
||||||
|
// The file is meant to be copied: take it into another program, adjust the
|
||||||
|
// configuration block below, hang `--update` and `--check-update` into the
|
||||||
|
// options — done. It needs nothing but the standard library, and apart from
|
||||||
|
// that block it brings no names that do not begin with "selfUpdate" or
|
||||||
|
// "update".
|
||||||
|
//
|
||||||
|
// It assumes the layout build.sh produces: one release per version, whose tag
|
||||||
|
// is the bare number (1.21.4, a leading "v" is allowed), holding one asset
|
||||||
|
// "<name>-<goos>-<goarch>" each — that is, exactly the files from ./bin. Under
|
||||||
|
// /api/v1/repos/<owner>/<repo>/releases/latest Gitea hands out the newest
|
||||||
|
// release that is neither a draft nor a prerelease; GitHub speaks the same
|
||||||
|
// route with different field names and is therefore not covered.
|
||||||
|
//
|
||||||
|
// Two things this copy does differently from the one in dns, both deliberate:
|
||||||
|
//
|
||||||
|
// - The release has to publish a "checksums.txt" — the file build.sh writes,
|
||||||
|
// in the format of `shasum -a 256`. What is downloaded is weighed against
|
||||||
|
// the sha256 named there for exactly this asset, and no checksum means no
|
||||||
|
// update. gbld had that property before it fetched from a gitea, and it
|
||||||
|
// does not give it up.
|
||||||
|
// - The TLS certificate is verified, as it is everywhere else. dns turns
|
||||||
|
// that off for machines with an outdated trust store; if you copy this
|
||||||
|
// file to such a machine, that is the line to revisit — knowing that
|
||||||
|
// anyone in the network path can then hand out both the binary and the
|
||||||
|
// checksum that matches it.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"hash"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ Configuration
|
||||||
|
|
||||||
|
var selfUpdate = selfUpdater{
|
||||||
|
repo: "https://git.fhi.mpg.de/mike/gbld",
|
||||||
|
asset: "gbld",
|
||||||
|
current: version, // from gbld.go, set by -ldflags
|
||||||
|
verify: []string{"-v"},
|
||||||
|
every: 24 * time.Hour,
|
||||||
|
quietEnv: "GBLD_NO_UPDATE",
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateChecksums is the asset holding the sha256 of every other asset, in the
|
||||||
|
// format of `shasum -a 256`. A release without it is a release this program
|
||||||
|
// refuses to install: what cannot be weighed is not put in place.
|
||||||
|
const updateChecksums = "checksums.txt"
|
||||||
|
|
||||||
|
type selfUpdater struct {
|
||||||
|
repo string // repo URL as in the browser: https://host/owner/repo
|
||||||
|
asset string // base name of the assets, "-<goos>-<goarch>" is added
|
||||||
|
current string // the running version
|
||||||
|
verify []string // trial run of the download; empty skips it
|
||||||
|
every time.Duration // how often to look on its own; 0 turns that off
|
||||||
|
quietEnv string // this environment variable set: keep quiet as well
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateRefreshFlag is the option the program calls itself with, in the
|
||||||
|
// background. It is deliberately absent from the help.
|
||||||
|
const updateRefreshFlag = "--update-refresh"
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ Looking by itself
|
||||||
|
|
||||||
|
// daily is the hook for the ordinary run of the program. It costs nothing: in
|
||||||
|
// the foreground the network is never touched. What comes back is the line
|
||||||
|
// pointing at a new version — or "", when there is nothing to say; what it
|
||||||
|
// looks like is up to the caller. Should the note be older than `every`, daily
|
||||||
|
// starts a background run on the side, whose answer the next call will find
|
||||||
|
// waiting.
|
||||||
|
func (u selfUpdater) daily() string {
|
||||||
|
if u.every <= 0 || os.Getenv(u.quietEnv) != "" || !updateOnTerminal() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
st := u.loadState() // no file: the zero value, hence due at once
|
||||||
|
|
||||||
|
if time.Since(st.Checked) >= u.every {
|
||||||
|
// The timestamp moves on before the asking, not after: otherwise two
|
||||||
|
// simultaneous runs start two queries, and a server that is not in the
|
||||||
|
// mood would get a new one on every call. If the note does not stay
|
||||||
|
// put, nothing is asked either — else an unwritable cache directory
|
||||||
|
// would mean one process per call.
|
||||||
|
st.Checked = time.Now()
|
||||||
|
if u.saveState(st) == nil {
|
||||||
|
u.spawnRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if st.Latest == "" || updateCompare(st.Latest, u.current) <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %s is available, run '%s --update'", u.asset, st.Latest, u.asset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// refresh is the background run: ask, write it down, stay quiet. The writing
|
||||||
|
// down is done by latest; if the query fails, the old state remains.
|
||||||
|
func (u selfUpdater) refresh() {
|
||||||
|
_, _ = u.latest()
|
||||||
|
}
|
||||||
|
|
||||||
|
// spawnRefresh calls this program once more, only to ask, and does not wait.
|
||||||
|
// Without a Wait the child is adopted by init when this process ends — it thus
|
||||||
|
// outlives the call, and the call's output stays untouched by it.
|
||||||
|
func (u selfUpdater) spawnRefresh() {
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cmd := exec.Command(exe, updateRefreshFlag)
|
||||||
|
cmd.Stdin, cmd.Stdout, cmd.Stderr = nil, nil, nil // everything to /dev/null
|
||||||
|
if cmd.Start() == nil {
|
||||||
|
cmd.Process.Release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The hint is meant for the person sitting there. Running in a pipe, in a
|
||||||
|
// script or under cron, the program neither asks nor says anything.
|
||||||
|
func updateOnTerminal() bool {
|
||||||
|
st, err := os.Stderr.Stat()
|
||||||
|
return err == nil && st.Mode()&os.ModeCharDevice != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------- Note
|
||||||
|
|
||||||
|
// updateState is what is left between two calls: when the last question was
|
||||||
|
// asked and what came of it.
|
||||||
|
type updateState struct {
|
||||||
|
Checked time.Time `json:"checked"`
|
||||||
|
Latest string `json:"latest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// The note lives in the cache directory, not in the configuration: if it gets
|
||||||
|
// lost, the only cost is asking once too early.
|
||||||
|
func (u selfUpdater) statePath() (string, error) {
|
||||||
|
dir, err := os.UserCacheDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Join(dir, u.asset, "update.json"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u selfUpdater) loadState() updateState {
|
||||||
|
var st updateState
|
||||||
|
path, err := u.statePath()
|
||||||
|
if err != nil {
|
||||||
|
return st
|
||||||
|
}
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return st
|
||||||
|
}
|
||||||
|
json.Unmarshal(b, &st) // a broken file counts as none
|
||||||
|
return st
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u selfUpdater) saveState(st updateState) error {
|
||||||
|
path, err := u.statePath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(st)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// By way of a file alongside, so that a simultaneous run never comes upon
|
||||||
|
// half a JSON.
|
||||||
|
tmp := path + ".new"
|
||||||
|
if err := os.WriteFile(tmp, b, 0o644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.Rename(tmp, path); err != nil {
|
||||||
|
os.Remove(tmp)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ The work
|
||||||
|
|
||||||
|
// check only looks and touches nothing.
|
||||||
|
func (u selfUpdater) check(w io.Writer) error {
|
||||||
|
rel, err := u.latest()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if updateCompare(rel.TagName, u.current) <= 0 {
|
||||||
|
fmt.Fprintf(w, "%s %s is up to date\n", u.asset, u.current)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "%s %s is available, running %s\n %s\n run '%s --update' to install it\n",
|
||||||
|
u.asset, rel.TagName, u.current, rel.HTMLURL, u.asset)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// install fetches the newest release and replaces the running file with it.
|
||||||
|
func (u selfUpdater) install(w io.Writer) error {
|
||||||
|
rel, err := u.latest()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if updateCompare(rel.TagName, u.current) <= 0 {
|
||||||
|
fmt.Fprintf(w, "%s %s is up to date\n", u.asset, u.current)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
want := u.assetName()
|
||||||
|
src := rel.asset(want)
|
||||||
|
if src == nil {
|
||||||
|
names := make([]string, len(rel.Assets))
|
||||||
|
for i, a := range rel.Assets {
|
||||||
|
names[i] = a.Name
|
||||||
|
}
|
||||||
|
return fmt.Errorf("release %s has no %q (only %s)", rel.TagName, want, strings.Join(names, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Before a byte is fetched: without a checksum to weigh it against there is
|
||||||
|
// nothing to install, and saying so now saves the download.
|
||||||
|
sum, err := u.checksum(rel, want)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot locate the running binary: %w", err)
|
||||||
|
}
|
||||||
|
// An installed gbld is often a symlink into ./bin. What should be replaced is
|
||||||
|
// the file behind it, not the link.
|
||||||
|
if real, err := filepath.EvalSymlinks(exe); err == nil {
|
||||||
|
exe = real
|
||||||
|
}
|
||||||
|
mode := os.FileMode(0o755)
|
||||||
|
if st, err := os.Stat(exe); err == nil {
|
||||||
|
mode = st.Mode().Perm()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "downloading %s %s (%s)\n", want, rel.TagName, updateSize(src.Size))
|
||||||
|
tmp, err := u.download(src, exe, mode, sum)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.Remove(tmp) // only bites when the renaming below falls through
|
||||||
|
|
||||||
|
if err := u.probe(tmp, rel.TagName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := updateReplace(tmp, exe); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "%s %s → %s, at %s\n", u.asset, u.current, rel.TagName, exe)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// assetName is what this build of this program is called in a release. Windows
|
||||||
|
// carries the suffix its loader insists on; everywhere else the bare name.
|
||||||
|
func (u selfUpdater) assetName() string {
|
||||||
|
name := fmt.Sprintf("%s-%s-%s", u.asset, runtime.GOOS, runtime.GOARCH)
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
name += ".exe"
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// checksum fetches the release's checksums.txt and picks out the line for this
|
||||||
|
// asset. Every way out of here that is not a sha256 is an error: a release
|
||||||
|
// without checksums, one that does not mention this asset, a line that is not
|
||||||
|
// a hash — none of them ends in an installed binary.
|
||||||
|
func (u selfUpdater) checksum(rel updateRelease, name string) ([]byte, error) {
|
||||||
|
src := rel.asset(updateChecksums)
|
||||||
|
if src == nil {
|
||||||
|
return nil, fmt.Errorf("release %s publishes no %s, refusing to install %s unchecked",
|
||||||
|
rel.TagName, updateChecksums, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resp, err := updateGet(ctx, src.URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) // a list of hashes, not a payload
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot read %s of release %s: %w", updateChecksums, rel.TagName, err)
|
||||||
|
}
|
||||||
|
return updateFindSum(body, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateFindSum reads the format of `shasum -a 256`: hash, blanks, name. The
|
||||||
|
// asterisk some tools put in front of the name in binary mode is not part of it.
|
||||||
|
func updateFindSum(body []byte, name string) ([]byte, error) {
|
||||||
|
for _, line := range strings.Split(string(body), "\n") {
|
||||||
|
f := strings.Fields(line)
|
||||||
|
if len(f) != 2 || strings.TrimPrefix(f[1], "*") != name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sum, err := hex.DecodeString(f[0])
|
||||||
|
if err != nil || len(sum) != sha256.Size {
|
||||||
|
return nil, fmt.Errorf("%s holds no usable sha256 for %s", updateChecksums, name)
|
||||||
|
}
|
||||||
|
return sum, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("%s names no checksum for %s", updateChecksums, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u selfUpdater) download(a *updateAsset, exe string, mode os.FileMode, sum []byte) (string, error) {
|
||||||
|
// The new file comes into being next to the old one: same filesystem, so
|
||||||
|
// the renaming at the end is one atomic step and not half a copy. It also
|
||||||
|
// comes into being before the first byte — a missing write permission ought
|
||||||
|
// to show up before a few megabytes have gone down the wire.
|
||||||
|
dir := filepath.Dir(exe)
|
||||||
|
f, err := os.CreateTemp(dir, "."+filepath.Base(exe)+".new")
|
||||||
|
if err != nil {
|
||||||
|
var pe *os.PathError // the path is in the message already
|
||||||
|
if errors.As(err, &pe) {
|
||||||
|
err = pe.Err
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("cannot write to %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
tmp := f.Name()
|
||||||
|
|
||||||
|
resp, err := updateGet(context.Background(), a.URL)
|
||||||
|
if err != nil {
|
||||||
|
f.Close()
|
||||||
|
os.Remove(tmp)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// The hash grows while the file is written: the bytes are weighed on their
|
||||||
|
// way past, not read back afterwards.
|
||||||
|
var h hash.Hash = sha256.New()
|
||||||
|
n, err := io.Copy(io.MultiWriter(f, h), resp.Body)
|
||||||
|
if cerr := f.Close(); err == nil {
|
||||||
|
err = cerr
|
||||||
|
}
|
||||||
|
if err == nil && a.Size > 0 && n != a.Size {
|
||||||
|
err = fmt.Errorf("got %d of %d bytes from %s", n, a.Size, a.URL)
|
||||||
|
}
|
||||||
|
if err == nil && !bytes.Equal(h.Sum(nil), sum) {
|
||||||
|
err = fmt.Errorf("%s does not match its published checksum: got %s, expected %s",
|
||||||
|
a.Name, hex.EncodeToString(h.Sum(nil)), hex.EncodeToString(sum))
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = os.Chmod(tmp, mode)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(tmp)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return tmp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// probe calls the freshly fetched runner once. That catches a file that is
|
||||||
|
// truncated, built for the wrong platform, or not executable in the first
|
||||||
|
// place, before it replaces the running one.
|
||||||
|
func (u selfUpdater) probe(path, tag string) error {
|
||||||
|
if len(u.verify) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
out, err := exec.CommandContext(ctx, path, u.verify...).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("the downloaded binary does not run: %w", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(out), strings.TrimPrefix(tag, "v")) {
|
||||||
|
return fmt.Errorf("the downloaded binary reports %q, expected %s",
|
||||||
|
strings.TrimSpace(string(out)), tag)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateReplace swaps the running file for the new one.
|
||||||
|
func updateReplace(tmp, exe string) error {
|
||||||
|
if err := os.Rename(tmp, exe); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Unix overwrites the file of a running program without complaint, Windows
|
||||||
|
// does not: there the old one has to be got out of the way first. Deleting
|
||||||
|
// it becomes possible when this process ends at the earliest — so the
|
||||||
|
// tidying up is allowed to fail.
|
||||||
|
old := exe + ".old"
|
||||||
|
os.Remove(old)
|
||||||
|
if err := os.Rename(exe, old); err != nil {
|
||||||
|
return fmt.Errorf("cannot replace %s: %w", exe, err)
|
||||||
|
}
|
||||||
|
if err := os.Rename(tmp, exe); err != nil {
|
||||||
|
os.Rename(old, exe) // back to how it was
|
||||||
|
return fmt.Errorf("cannot replace %s: %w", exe, err)
|
||||||
|
}
|
||||||
|
os.Remove(old)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------- Gitea
|
||||||
|
|
||||||
|
type updateRelease struct {
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
HTMLURL string `json:"html_url"`
|
||||||
|
Assets []updateAsset `json:"assets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type updateAsset struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
URL string `json:"browser_download_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// asset picks one file out of a release by name, nil when it is not there.
|
||||||
|
func (r updateRelease) asset(name string) *updateAsset {
|
||||||
|
for i := range r.Assets {
|
||||||
|
if r.Assets[i].Name == name {
|
||||||
|
return &r.Assets[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u selfUpdater) latest() (updateRelease, error) {
|
||||||
|
base, err := u.apiBase()
|
||||||
|
if err != nil {
|
||||||
|
return updateRelease{}, err
|
||||||
|
}
|
||||||
|
// The question is a small one; if it hangs, it does not hang for long. The
|
||||||
|
// generous time limit of updateClient is meant for the download.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resp, err := updateGet(ctx, base+"/releases/latest")
|
||||||
|
if err != nil {
|
||||||
|
return updateRelease{}, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var rel updateRelease
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil {
|
||||||
|
return updateRelease{}, fmt.Errorf("unexpected answer from %s: %w", base, err)
|
||||||
|
}
|
||||||
|
if rel.TagName == "" {
|
||||||
|
return updateRelease{}, fmt.Errorf("%s has no releases", u.repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every question that succeeds fills the note — no matter whether it came
|
||||||
|
// from --update, from --check-update or from the background run.
|
||||||
|
u.saveState(updateState{Checked: time.Now(), Latest: rel.TagName})
|
||||||
|
return rel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiBase turns https://host/owner/repo into the API root of the repo.
|
||||||
|
func (u selfUpdater) apiBase() (string, error) {
|
||||||
|
bad := fmt.Errorf("repo %q: expected https://host/owner/repo", u.repo)
|
||||||
|
|
||||||
|
ref, err := url.Parse(strings.TrimSuffix(strings.TrimSuffix(u.repo, "/"), ".git"))
|
||||||
|
if err != nil || ref.Host == "" {
|
||||||
|
return "", bad
|
||||||
|
}
|
||||||
|
parts := strings.Split(strings.Trim(ref.Path, "/"), "/")
|
||||||
|
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
||||||
|
return "", bad
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s://%s/api/v1/repos/%s/%s", ref.Scheme, ref.Host, parts[0], parts[1]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// One time limit for all of it: the look costs a few hundred milliseconds, the
|
||||||
|
// download a few megabytes — both may hang, but not forever.
|
||||||
|
//
|
||||||
|
// Nothing is turned off here: the certificate of the gitea is verified like any
|
||||||
|
// other. Together with the checksum from the release, that is what stands
|
||||||
|
// between this program and a binary someone else picked — the probe below is
|
||||||
|
// none of it, a hostile binary prints whatever version string is asked of it.
|
||||||
|
var updateClient = &http.Client{
|
||||||
|
Timeout: 5 * time.Minute,
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateGet(ctx context.Context, target string) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", "selfupdate.go (+"+runtime.GOOS+"/"+runtime.GOARCH+")")
|
||||||
|
|
||||||
|
resp, err := updateClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
resp.Body.Close()
|
||||||
|
return nil, fmt.Errorf("GET %s: %s", target, resp.Status)
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ Numbers
|
||||||
|
|
||||||
|
// updateCompare compares two versions component by component, numerically, so
|
||||||
|
// that 2.1.10 lands behind 2.1.9 and not in front of it. A leading "v" does not
|
||||||
|
// count, missing places count as 0 (2.1 == 2.1.0), and a suffix on the number
|
||||||
|
// makes the version older, not newer (2.1.6-rc1 < 2.1.6). The result is the one
|
||||||
|
// of strings.Compare: -1, 0, 1.
|
||||||
|
func updateCompare(a, b string) int {
|
||||||
|
as := strings.Split(strings.TrimPrefix(a, "v"), ".")
|
||||||
|
bs := strings.Split(strings.TrimPrefix(b, "v"), ".")
|
||||||
|
|
||||||
|
for i := 0; i < len(as) || i < len(bs); i++ {
|
||||||
|
x, y := "0", "0"
|
||||||
|
if i < len(as) {
|
||||||
|
x = as[i]
|
||||||
|
}
|
||||||
|
if i < len(bs) {
|
||||||
|
y = bs[i]
|
||||||
|
}
|
||||||
|
if c := updateComparePart(x, y); c != 0 {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateComparePart(a, b string) int {
|
||||||
|
na, ra := updateSplitNum(a)
|
||||||
|
nb, rb := updateSplitNum(b)
|
||||||
|
switch {
|
||||||
|
case na != nb:
|
||||||
|
if na < nb {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
case ra == rb:
|
||||||
|
return 0
|
||||||
|
case ra == "": // 2.1.6 is finished, 2.1.6-rc1 is not yet
|
||||||
|
return 1
|
||||||
|
case rb == "":
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return strings.Compare(ra, rb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateSplitNum separates "10-rc1" into 10 and "-rc1".
|
||||||
|
func updateSplitNum(s string) (int, string) {
|
||||||
|
i := 0
|
||||||
|
for i < len(s) && s[i] >= '0' && s[i] <= '9' {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
n, _ := strconv.Atoi(s[:i])
|
||||||
|
return n, s[i:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateSize is deliberately a small formatting of its own and not a helper
|
||||||
|
// from the toolbox — the file is meant to stand on its own.
|
||||||
|
func updateSize(b int64) string {
|
||||||
|
const k = 1024
|
||||||
|
switch {
|
||||||
|
case b > k*k:
|
||||||
|
return fmt.Sprintf("%.1f MB", float64(b)/k/k)
|
||||||
|
case b > k:
|
||||||
|
return fmt.Sprintf("%.1f KB", float64(b)/k)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%d B", b)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -15,96 +14,28 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"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/core"
|
||||||
"github.com/AlecAivazis/survey/v2/terminal"
|
"github.com/AlecAivazis/survey/v2/terminal"
|
||||||
"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/mgutz/ansi"
|
||||||
"github.com/minio/selfupdate"
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
var tbversion = "0.7.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
|
// Updating oneself lives in selfupdate.go — one file per program, configured at
|
||||||
prg := prgname()
|
// its head, driven by the --update and --check-update options. It fetches from
|
||||||
client := &http.Client{Timeout: HTTPTIMEOUT}
|
// the releases of the gitea and weighs every download against the checksum
|
||||||
|
// published beside it.
|
||||||
body, err := fetch(client, URL+"/version.txt", 1<<20) // update server unreachable: never block the build
|
|
||||||
if err != nil { return }
|
|
||||||
|
|
||||||
lversion := strings.TrimSpace(string(body)) // exactly as published - it names the artifact
|
|
||||||
sv_version, err := semver.NewVersion(version)
|
|
||||||
if err != nil { return }
|
|
||||||
sv_lversion, err := semver.NewVersion(lversion)
|
|
||||||
if err != nil { return }
|
|
||||||
if !sv_lversion.GreaterThan(sv_version) { return }
|
|
||||||
|
|
||||||
if !Yesno(SF("new '%s' version found (%s -> %s), update now?",prg,sv_version,lversion),true,false) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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(client *http.Client, url string, checksum []byte) error { // --------------------------- do update
|
|
||||||
resp, err := client.Get(url)
|
|
||||||
if err != nil { return err }
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode != http.StatusOK { return SE("server returned status: %v", resp.Status) }
|
|
||||||
return selfupdate.Apply(resp.Body, selfupdate.Options{Checksum: checksum})
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
func checkaccess(NETS []string) { // ------------------------------------------------- check ip net based access
|
||||||
match:=0;
|
match:=0;
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
1.22.0
|
||||||
Reference in New Issue
Block a user