Anything Version Manager - A version manager to temporarily replace any program with any script on your PATH.
If your developer machine is like mine, it's filled with small CLI tools and utilities that are installed directly on the machine with a package manager like Homebrew.
Examples of such tools in my experience can be jq, yq, buf, protoc, golangci-lint, gosec and mockery.
When working in (or for) a company on a project, they might use some of the same set of tools and utilities, but different (typically older) versions.
The dilemma is then; Do you adapt your precious developer machine on a per-company or per-project basis? How do you support having different versions of the same programs depending on your workload?
Perhaps the most obvious solution is to run the tools inside a Docker image, but if you have to use a custom bash function or alias to run a secondary version of a program, it might not work with a project's checked-in scripts in Makefiles or package.json (that rely on executing the programs as they are installed by default), without having to change the scripts to use your executable names which then conflicts with other developers or build servers using the same scripts.
Anyver is a small utility that aims to remedy this problem by managing a set of alternative scripts for any given program name. When a script is activated by Anyver, an executable (bash) script with the same name as the program will be put in ~/.anyver/apps. The content of this script file will be whatever script content you have configured in a global Anyver yaml config file.
For example, if I want to create a spoofed version of jq, I can run anyver use jq the-other-version. If the script the-other-version has the content docker exec --workdir $PWD my-toolbox-image jq $@, then that will be the content of the file ~/.anyver/apps/jq created by Anyver.
Since I've made sure that ~/.anyver/apps is always first on my PATH environment variable, I know that Anyver's spoofed version of the executable will "win" when a jq command is executed in a shell (the shell finds the spoofed executable first).
To restore jq to its original executable (/usr/local/bin/jq in my case), I'll run anyver restore jq and Anyver will remove the file ~/.anyver/apps/jq so that the next (default) occurrance of jq on my PATH will be found and executed.
Therefore, Anyver can replace any program with any script and serve as a version manager for anything.
Either install it to your $GOPATH/bin and make sure $GOPATH/bin is on your PATH:
go install github.com/eaardal/anyver
Or clone the repo, build it and make sure the built executable is somewhere on your PATH:
go build -o anyver ./... && cp ./anyver <DIR_ON_PATH>/anyver
In a terminal, run:
anyver init
This will create a default ~/.anyver/config.yaml file.
Add this to your .zshrc, .bashrc or other shell config script:
export PATH=$HOME/.anyver/apps:$PATHThe path is not required to be first-first, but it should be in your PATH before wherever your programs are usually found, such as /usr/local/bin and /usr/bin. This is crucial for anyver to work.
You can inspect your PATH with: echo $PATH.
NAME:
Anything Version Manager
USAGE:
Anything Version Manager [global options] command [command options] [arguments...]
COMMANDS:
config Print the entire Anyver config file as-is
use Set the given version as the active command when running the app
restore Removes the Anyver shortcut which should make the system's default executable the main one
restore-all Removes all active Anyver shortcuts which should make the system's default executables the main ones for all apps
context Manage multiple apps in a context
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--help, -h show help (default: false)
Prints the file ~/.anyver/config.yaml as it's found on disk.
Example:
> anyver config
active:
jq: _system
mockery: my-company
apps:
jq:
my-company: docker run --rm -it --volume $PWD:$PWD --workdir $PWD my-toolbox-image jq $@
mockery:
my-company: docker run --rm -it --volume $PWD:$PWD --workdir $PWD my-toolbox-image mockery $@
personal-project: docker run --rm -it --volume $PWD:$PWD --workdir $PWD my-private-image mockery $@
contexts:
default:
jq: _system
mockery: _system
all-apps-my-company:
jq: my-company
mockery: my-company
hobby-tinkering:
mockery: personal-projectActivates an app alias. Anyver will find the script for the given app name and make a executable file with that app name in ~/.anyver/apps.
Example:
Given the config file
active:
jq: _system
mockery: my-company
apps:
jq:
my-company: docker run --rm -it --volume $PWD:$PWD --workdir $PWD my-toolbox-image jq $@
mockery:
my-company: docker run --rm -it --volume $PWD:$PWD --workdir $PWD my-toolbox-image mockery $@
contexts:
default:
jq: _system
mockery: _system
all-apps-my-company:
jq: my-company
mockery: my-companyAlias and verify an app:
# No aliased apps before we start
> ls ~/.anyver/apps
(no files found)
# Using system/default jq
> which jq
/usr/local/bin/jq
# Tell Anyver to make an alias for jq
> anyver use jq my-company
Now using version "my-company" for app "jq"
# Now there is 1 aliased app
> ls ~/.anyver/apps
jq
# Content of the file is our script
> cat ~/.anyver/apps/jq
docker run --rm -it --volume $PWD:$PWD --workdir $PWD my-toolbox-image jq $@
# Using aliased jq from Anyver
> which jq
/Users/<name>/.anyver/apps/jqRemoves the Anyver shortcut which should make the system's default executable the main one.
Will simply delete the file under ~/.anyver/apps if it exists.
Example:
> anyver restore jq
Restored app "jq"
Removes all active Anyver shortcuts which should make the system's default executables the main ones for all apps.
Same as restore, but it deletes all files under ~/.anyver/apps instead of just one.
Alias all apps listed under the given context
Context lets you define multiple apps under one name. Useful if there are multiple apps you have to switch between when working on different projects.
Example:
Given the config file
active:
jq: _system
mockery: my-company
apps:
jq:
my-company: docker run --rm -it --volume $PWD:$PWD --workdir $PWD my-toolbox-image jq $@
mockery:
my-company: docker run --rm -it --volume $PWD:$PWD --workdir $PWD my-toolbox-image mockery $@
contexts:
default:
jq: _system
mockery: _system
all-apps-my-company:
jq: my-company
mockery: my-companyApplying a context
> anyver context use all-apps-my-company
Now using version "my-company" for app "jq"
Now using version "my-company" for app "mockery"
Same as restore or restore-all but only deletes apps in the given context.
My current ~/.anyver/config.yaml:
active:
all: _system
buf: rp
easyjson: rp
golangci-lint: rp
gosec: rp
grpcurl: _system
kafkacat: _system
mockery: rp
protoc: rp
semtag: rp
apps:
buf:
rp: docker exec --workdir "$PWD" toolbox buf $@
easyjson:
rp: docker exec --workdir "$PWD" toolbox easyjson $@
golangci-lint:
rp: docker exec --workdir "$PWD" toolbox golangci-lint $@
gosec:
rp: docker exec --workdir "$PWD" toolbox gosec $@
grpcurl:
rp: docker exec --workdir "$PWD" toolbox grpcurl $@
kafkacat:
rp: docker exec --workdir "$PWD" toolbox kafkacat $@
mockery:
rp: docker exec --workdir "$PWD" toolbox mockery $@
protoc:
rp: docker exec --workdir "$PWD" toolbox protoc $@
semtag:
rp: docker exec --workdir "$PWD" toolbox semtag $@
contexts:
default:
buf: _system
easyjson: _system
golangci-lint: _system
gosec: _system
kafkacat: _system
mockery: _system
protoc: _system
semtag: _system
sops: _system
company:
buf: rp
easyjson: rp
golangci-lint: rp
gosec: rp
grpcurl: rp
kafkacat: rp
mockery: rp
protoc: rp
semtag: rpNotes:
- "rp" is a shortname for my team at this company. The name could be anything.
- I could also have more than just one override for each executable.
grpcurlandkafkacatis currently using the machine-installed executable in /usr/bin (using_system). If I were to runanyver use grpcurl rpit would use the specified Docker command instead.
A problem I encountered is that if another tool/app/script tries to execute the Anyver aliased app not through a shell interpreter, it might fail thinking that the exeutable that Anyver is aliasing is corrupt or of the wrong architecture.
I experienced this when generating Go code with go generate ./.... I have the tool mockery aliased using Anyver so that it runs mockery from our CI build pipeline image via docker exec --workdir "$PWD" $image mockery $@. So my ~/.anyver/apps/mockery file was just the docker exec command. When I encountered Go code that was annotated like:
//go:generate mockery --name FooService
type FooService interface {}It crashed with the error:
api/service/foo/foo_service.go:13: running "mockery": fork/exec /<redacted>/.anyver/apps/mockery: exec format error
The exec format error indicated that it thought my mockery exeutable was corrupt or of the wrong architecture (amd64 while I'm on arm64 for example) or something more low level like that.
After some debugging I found that wrapping the docker exec command in exec solved it:
exec $(source docker exec --workdir "$PWD" $image mockery "$@")This replaces the shell process with the docker command so that when the script is executed, it effectively runs the Docker command as if it were the binary itself. This therefore avoids any shell interpretation issues caused by running the command via go generate.