upd (Go)
A port of the Perl upd one directory up. Same command line, same config file,
same state and cache files - the two can be used interchangeably on the same
machine.
Build
./build.sh
builds every platform into ./bin, statically linked and stripped, and bumps
the patch version by 0.0.1 on each run. version.txt holds the version just
built; the same number goes into the binaries via -ldflags -X main.version,
so upd --version and the release tag always agree. One platform only:
PLATFORMS="linux/amd64" ./build.sh
The asset names in ./bin - upd-<goos>-<goarch> - are exactly what
--update looks for in a release, so a release is ./bin uploaded as it is.
A plain go build -o ~/bin/upd . still works; it just reports the fallback
version from main.go.
go test ./... runs the asset selection against the release JSON in
../t/corpus, the same corpus t/select.t uses, and fails if the two
implementations start disagreeing.
The first one
upd --update needs an upd to run it. loadupd.sh fetches that first one -
POSIX sh, curl or wget, no upd required:
./loadupd.sh # into the current directory
./loadupd.sh ~/bin # into a directory of your choosing
It reads the newest release, picks the asset matching uname -s/uname -m,
runs it once before putting it in place, and says if the directory is not in
$PATH. $UPD_REPO_URL points it at a mirror. From there on upd takes over.
Updating itself
upd installs itself the same way it installs everything else - same forge layer, same CA fallback, same checksum check, same atomic replace:
upd --version # what is running
upd --check-update # look, change nothing
upd --update # download the newest release and replace the binary
--update replaces the file the running binary actually is, following a
symlink into ./bin to the file behind it. It refuses before downloading if
that directory is not writable, and it runs the downloaded binary once with
--version before letting it take over, so a truncated file or one for the
wrong platform never replaces a working one. --force reinstalls the current
version, --dry-run says what it would do.
Beyond that, an ordinary run looks for a new release once a day, in the background, and mentions it on stderr:
Note: upd 2.0.7 is available, run 'upd --update'.
The look never happens in the foreground - the run itself is never slowed down
or made to depend on the network - and never when stderr is not a terminal, so
cron and pipelines stay silent. UPD_NO_UPDATE_CHECK=1 switches it off
altogether. The note lives in ~/.cache/upd/selfupdate.json.
The mechanism is the one in dx; here it goes through upd's own machinery
instead of bringing its own HTTP client.
Why a port
The Perl version needs curl or wget for HTTPS, which is exactly what breaks on
an older system: a curl linked against OpenSSL 1.0.x cannot complete a
handshake with a server that requires TLS 1.2, and there is nothing upd can do
about it beyond falling back to wget. The Go binary brings its own TLS stack,
so the transport question disappears - along with --stderr juggling, exit
code translation and the wget fallback.
Certificates
Verification uses the system CA store. If that store does not know the issuer -
the usual case on a machine whose ca-certificates package predates Let's
Encrypt's ISRG roots - upd says so once and retries with the Mozilla CA list
embedded in ca-bundle.pem, so the binary stays self-sufficient. Refresh that
file with:
curl -o ca-bundle.pem https://curl.se/ca/cacert.pem
--cacert FILE (or $UPD_CACERT) replaces the system store, for a private CA.
--insecure skips verification altogether and warns on every run. Both switch
the automatic fallback off: an explicit choice stays the choice.
Differences
| Perl | Go | |
|---|---|---|
| TLS | curl / wget / IO::Socket::SSL | built in |
| CA roots | whatever the system has | system, with the Mozilla list as fallback |
--cacert, --insecure |
– | yes |
tar, unzip |
external | in-process |
.gz, .bz2 |
external | in-process |
.xz, .zst, .lz4, .7z |
external | external (unchanged) |
| Archive paths | trusted to tar |
checked against traversal |
version-flag= in the config file |
overwritten by its own default | honoured |
Behaviour that deliberately stayed identical: asset scoring (same picks on the
whole corpus), the --install file-or-directory rule, state and cache file
layout, exit codes 0/1/2/10.