In this README: Introduction | Quick start | Using this plugin | Variables provided by this plugin | An example | Contributing to this plugin | Why does this plugin exist? | Technical details
asdf-ace
is a plugin for the ASDF version manager that
installs different version of arbitrary binaries as configured by the user,
and lets ASDF switch between those versions.
To use it, follow the Quick start guide. This guide:
- tells you about the small set of pre-requisites that you'll need to install first.
- takes you through configuring this plugin via a simple file in your home directory. This file is your personally-unique list of all the tools that this plugin is allowed to install on your machine, and it can contain any tool that you find useful. It's not limited to tools that appear in this plugin's examples directory - that directory is just a starting point.
- shows you how to use ASDF to add this plugin for each tool you want to install
- finally, shows you how to use ASDF to install a specific version of a tool and make it available to your user
Before doing that, however, folks who care about the security of their systems should expand and read the following Security Klaxon section:
πͺ Security Klaxon π¨π¨π¨
This plugin's name is a "playful" reminder that the security model of ASDF is very much "buyer beware". ASDF requires you to trust plugin authors, as plugins execute code directly on your machine. This plugin goes one step further: you must explicitly configure this plugin only to download binaries that you trust.
The onus is on you, the user, to use only those binaries that you trust.
This plugin can only download binaries that you have explicitly told it about.
You should not use this plugin simply because someone tells you to. You should know what problem you're trying to solve by using it, and should understand the links in the chain of trust that you're relying on.
A later version of this plugin will deal with the thorny issue of validating downloads. For now, this issue is in the user's hands -- your hands: only tell this plugin to download binaries from sites, projects and users that you trust.
- Have a working ASDF install.
- Find a tool that you want to install that publishes pre-compiled binaries either as direct downloads, or inside compressed tarballs or zip archives.
- Grab the appropriate download URL for your machine and operating system.
- Install the
CUE
CLI. - Install
curl
, and optionally either ofunzip
ortar
, depending on the format you're downloading. - Configure the file
$XDG_CONFIG_HOME/asdf-ace/tools.cue
as described below with an appropriate tool setup:- the tool's name is only an informative alias, and doesn't have to match
anything specific about the tool you're installing. In this quick start,
we imagine it to be
example_tool_name
. - the
source
value should be the URL you grabbed, above, with any version components swapped out for\(version.oc)
. - the
creates
value (either a simple string for direct binary downloads, or a key/value pair for tarball/zip archives and their contents) defines the name of the binary you want to make available on your machine. In this quick start, we imagine this to bemy_tool
.
- the tool's name is only an informative alias, and doesn't have to match
anything specific about the tool you're installing. In this quick start,
we imagine it to be
πͺ Example config file
package asdface
v0: tools:{
example_tool_name: #TarGz & {
source: "https://example.com/a_useful_project/releases/\(version.oc)/downloads/project-linux-amd64.tar.gz"
create: my_tool: "from/this/file/in/the/tarball"
}
}
-
Install this plugin:
asdf plugin add example_tool_name https://github.com/jpluscplusm/asdf-arbitrary-code-execution
-
Install a specific version of the tool:
asdf install example_tool_name v1.2.3
-
Tell ASDF to make this specific version available:
asdf global example_tool_name v1.2.3
-
Use the tool:
my_tool --help
It's well worth reading the configuration section and example to discover how you can make your config more portable, and how you can make it available for other folks to use!
This tool hasn't yet reached version 1.0, which means it's still experimental and its interface may change. We're working towards defining a stable interface, so your feedback about using the current interface will help us make the right choices, and is extremely welcome!
Required:
curl
cue
(see below)
Optional, depending on which packaging formats are referenced:
- Compressed tarballs:
tar
andgunzip
- Zip archives:
unzip
- Direct binary downloads: no additional pre-requisites
This plugin requires the cue
CLI in order to work.
If you don't already have it on your machine, it can be easily installed from its official release binaries on Windows, macOS, and Linux, or installed from source on any other platform that Go supports.
I'm well aware of the irony of this plugin -- a tool to automate the installation of arbitrary binaries off the internet -- requiring its users to download a binary manually ... but this is the last one you should ever need to fetch!
Make the cue
CLI available in your $PATH
, however you usually do that.
If you're installing from CUE's official release binaries then it's an easy 3-step process:
πͺ Installing the `cue` CLI in 3 easy steps
Here's how to install cue
from a package published by the CUE
project:
-
download the appropriate .tar.gz or .zip package for your operating system and machine from their GitHub releases page, near the bottom, under "Assets".
-
unpack just the
cue
binary from the package.-
you don't need the
doc/
directory that's also included in the package. -
for example:
tar xfz path/to/the/package/you/just/downloaded.tar.gz cue
-
-
move the
cue
binary into a directory that's in your$PATH
.-
$HOME/bin/
will probably work, as would/usr/local/bin/
(but it's your machine, so it's up to you!). -
for example:
mkdir -p ~/bin/ mv -i cue ~/bin/
-
If this is the first binary you've ever put in
$HOME/bin/
, you'll probably need to close and re-open your terminal to pick it up. Test this out by runningcue version
and seeing if your shell tells you "command not found". If so, close and re-open the terminal window.
-
If you find doing this even slightly annoying, then congratulations: you're in the right place! The entire purpose of this ASDF plugin is to avoid having to do this slightly annoying process ever again!
Each tool you configure can refer to either a compressed tarball, a zip archive, or a single unpackaged binary.
A compressed tarball or zip archive can have multiple binaries extracted from them. An unpackaged binary can only provide that single binary.
Populate the file $XDG_CONFIG_HOME/asdf-ace/tools.cue
with 1 or more tool's
information inside the top-level v0
key ("v0" reflects the unstable nature of
this plugin, pre-1.0). If $XDG_CONFIG_HOME
is unset (for instance if the tool
is being used on a non-Linux OS, or the XDG conventions are not being used),
the configuration file should be placed at $HOME/.config/asdf-ace/tools.cue
.
Indicate that each tool is either a #TarGz
, a #Zip
, or a #BinaryDownload
,
as demonstrated here. NB The & {
suffix is a vital part of the config!
package asdface
v0: {
tools: {
tool_name: #BinaryDownload & {
source: "https://example.com/path/to/binary/containing/\(version.oc)/and/\(go.os.lc)/and/\(go.arch.uc)/vars"
create: "binary_filename_we_want_to_use_in_the_shell"
}
"tool-names-containing-hyphens-need-to-be-in-quotes": #TarGz & {
source: "https://example.com/path/to/file/containing/\(version.oc)/and/\(go.os.lc)/and/\(go.arch.uc)/vars.tgz"
create: binary_filename_we_want_to_use_in_the_shell: "path/to/file/in/tarball"
create: {
"more-binaries-we-want-to-use-this-time-containing-hyphens": "path/to/this/file/in/the/same/tarball"
simpleFilename: "fileAtRootOfTarball"
}
}
someTool: #Zip & {
source: "https://example.com/path/\(version.oc)/withvars/\(go.os.lc)/and/\(go.arch.uc)/archive-\(version.oc).zip"
create: {
binary_name: "path/to/file/in/ziparchive"
binary_name_2: "path/to/this/file/in/the/same/ziparchive"
simpleFilename: "fileAtRootOfArchive"
}
}
}
}
Any string can contain variables, expressed in the configuration language CUE. Expand the next πͺ section for an incomplete crash course in CUE, its strings, and variables.
πͺ CUE: an incomplete, 90 second guide
Because this plugin's configuration is written in CUE, you have all the facilities of CUE available when writing your configuration.
But don't let that put you off - you don't need to learn a whole new language!
In the vast majority of cases you'll only
need to use CUE's \(variable)
interpolation
to set things up correctly, and this syntax is extremely plain and
straightforward.
Here's a really simple guide to the features of CUE that you'll probably need to use.
This plugin requires your configuration file to declare itself, on its first
line, to be part of the asdface
package in order to work correctly.
package asdface
Note that this package name doesn't contain a hyphen.
CUE is a hierarchy of key:value pairs, nested as deeply as you need.
CUE keys are strings. If they only contain only alphanumerics
([a-z][A-Z][0-9]
) and underscores then they don't need to be quoted. If they
contain hyphens, spaces, or other punctuation, then they have to be contained
in double quotes.
key: ...
key1: ...
a_key_with_underscores: ...
"a key with spaces": ...
"a-key-with-hyphens": ...
Nested keys can also be quoted or unquoted strings, depending on their alphanumeric contents, as above. Curly braces are used to indicate nesting.
key: {
a_nested_key: {
"a-third-level-key": {
"A Deeply nested key": ...
}
}
}
CUE values can be any type. This plugin require leaf node values to be strings.
This plugin reads its configuration from keys inside the v0.tools
hierarchy:
v0: {
tools: {
...
}
}
String values are contained in double quotes.
key: "a key value"
Line comments extend from a double-forward-slash to the end of the line.
// this is a comment on a line by itself
key: "a value" // this is a comment alongside a key
Strings can be concatentated with the +
operator.
key: "a value " + "split in 2" // produces ...
key: "a value split in 2"
Both of the above definitions of key
can co-exist in the same config
simultaneously, because they agree with one another. CUE config is evaluated
order-independently, except where inherently ordered entities are involved,
such as arrays. This plugin does not require you to use arrays.
Strings can be split across multiple lines at any point, any number of times.
Indentation is unimportant, but the cue fmt [filename]
command will reformat
your config file to the "official" standards, if you ask it to!
key: "a long value that " +
"needs to be split " +
"across " +
"multiple lines"
key: "a long value that needs to be split across multiple lines"
As above, because both definitions of key
agree with one another they are
able to co-exist in the same CUE configuration. However, the 2nd definition is
not required - it is included here solely to demonstrate the end result. It
would be entirely valid if it were included, however.
Strings can include variables that are interpolated inline, using \(value)
syntax.
key1: "a value"
key2: "this string also contains \(key1)"
key2: "this string also contains a value"
Because CUE evaluation is order-independent, assignment can happen after use.
key1: "this string contains \(key2)"
key2: "another string"
key1: "this string contains another string"
A single string can use both interpolation and concatentation.
key1: "value"
key2: "this string contains a \(key1) and also another " + key1
key2: "this string contains a value and also another value"
The variables that this plugin provides can be interpolated or concatenated into strings, in all the ways laid out above.
For example, given the variable go.os.lc
containing the string "linux"
...
key: "the/current-os/is/\(go.os.lc)/" // results in
key: "the/current-os/is/linux/"
All variables contain 2 components: a selector and a modifier, separated by a
.
(period).
This is the set of valid selectors, which will expand as we discover more useful values:
version
: the literal version requested by the useruname.m
: the output ofuname -m
- the machine's processor architectureuname.s
: the output ofuname -s
- the machine's operating systemgo.os
: the Go language's concept of the machine's running OSgo.arch
: a reasonable (but incomplete!) approximation of Go'sruntime.GOARCH
concept. Please do submit a PR (or file an Issue) if this could be improved in any way!
Modifiers indicate the specific upper/lower/etc case version of the selector's value that you want to use. You can use the following modifiers on all selectors:
oc
- original case: the exact string that the operating system gave usuc
- upper case: the.oc
value, with all upper-case characterslc
- lower case: the.oc
value, with all lower-case characterstc
- title case: the.oc
value, with all words title-cased -this is not very useful, as title-casing works on word boundaries- it might be useful to get the string "Linux" from the input "linux" or "Amd64" from "amd64" or "Bsd" from "BSD"
cc
- camel case: the.oc
value, with all words camel-cased- even less useful than title-cased. If this IS useful, please do PR an example to this doc!
Combining all the selectors with all the modifiers means that there are currently 25 variables available to interpolate into your configuration keys:
case>| original upper lower title camel
---------+--------------------------------------------------------------------
Version | version.oc version.uc version.lc version.tc version.cc
CPU | uname.m.oc uname.m.uc uname.m.lc uname.m.tc uname.m.cc
OS | uname.s.oc uname.s.uc uname.s.lc uname.s.tc uname.s.cc
GOOS | go.os.oc go.os.uc go.os.lc go.os.tc go.os.cc
GOARCH | go.arch.oc go.arch.uc go.arch.lc go.arch.tc go.arch.cc
So, for example, to reference a binary that's downloadable for your specific machine from
https://example.com/a_useful_project/releases/v1.2.3/downloads/project-linux-amd64
... the smallest reasonable source
line that you could use is this (notice we
can split the value across lines, to make it more readable):
source: "https://example.com/a_useful_project/releases/" +
"\(version.oc)/downloads/project-linux-amd64"
This is because at least one variant of the version
selector should (must!)
be present in the url
key, because the $XDG_CONFIG_HOME/asdf-ace/tools.cue
file isn't the final arbiter of which version ASDF will install. No: ASDF
(and therefore you, the user) is in charge of WHICH version gets installed, not
this config file.
The config file teaches ASDF, via this plugin, how to download DIFFERENT versions. If the version is hardcoded in the config then ... what's the point?
HOWEVER, this source
line example, above, only works to switch between
versions on your specific machine: a machine running Linux and containing an
x86_64
/amd64
chip.
In order to make this config more portable, so you could re-use it on different
operating systems and physical hardware, you'd need to swap more fixed elements
for variables in the source
line. All the examples in this plugin's examples
directory use this feature, so that you can simply copy and paste
any of the examples into your $XDG_CONFIG_HOME/asdf-ace/tools.cue
file, and
install the appropriate tool instantly. If you're going to PR an example into
this plugin, so other people can benefit from your effort, please make the
example as portable as you can!
To demonstrate doing this, let's pretend that you check your example
a_useful_project
's Downloads page and discover that the project publishes
binaries for 3 different system types, on these 3 URL path suffixes:
paths relative to the prefix "https://example.com/a_useful_project":
/releases/\(version.oc)/downloads/project-linux-x86_64
/releases/\(version.oc)/downloads/project-darwin-arm64
/releases/\(version.oc)/downloads/project-darwin-x86_64
In addition to the version.oc
variable that you used (so that ASDF can switch
versions for you) there are 2 more variable components of the URL: the
operating system (linux
vs darwin
(Mac OS)) and the machine architecture
(x86_64
vs arm64
).
These variables are already available in the selectors uname.s
(operating
system) and uname.m
(architecture). Putting them in place, we end up with a
single source
line that works on any machine type for which
a_useful_project
has published binaries:
source: "https://example.com/a_useful_project/releases/" +
version.oc +
"/downloads/" +
"project-\(uname.s.lc)-\(uname.m.lc)"
Notice that we split the long line to make it easier to read, and that we mix our use of CUE's string interpolation syntax with simply concatenating string values togther, where that makes more sense. This plugin doesn't care about how you use CUE to assemble a value - it only cares that it can download and process the resulting URL and archive :-)
Next, we add detail in the create
key about the name of the binary we want to
be able to run on our machine (useful
). We also wrap it up in the config
hierarchy that the plugin requires:
v0: tools: a_useful_project: #BinaryDownload & {
source: "https://example.com/a_useful_project/releases/" +
version.oc +
"/downloads/" +
"project-\(uname.s.lc)-\(uname.m.lc)"
create: "useful"
}
After seeding the config file with the neccessary information about the tool we want to install, we can install the binary.
NB make sure you've installed the CUE cli -- an easy, small, single-binary download -- before carrying on!
asdf plugin add a_useful_project \
https://github.com/jpluscplusm/asdf-arbitrary-code-execution
asdf install a_useful_project v1.2.3
... and then, to tell ASDF to make that version available to your shell:
asdf global a_useful_project v1.2.3
After running this, the (fake!) command useful
would be available, with the
requested version having been downloaded and the correct binary having been
selected for the machine you're using.
v1.2.3
in both the commands, above, is where you tell ASDF which version
you want to install.
Currently, this tool doesn't list remote versions, or know the latest/stable version that's available. You need to know and specify the exact version that you want to install.
If you have an example of a tool that can installed by this plugin, we'd love to hear about them! If we can make the example portable enough, it'll be added to the example library of tools that users can simply copy'n'paste into their config files.
Please start an "Ideas" Discussion with as much detail as you have about the tool, and how you've configured this plugin to install it.
If you feel like opening a PR to add the tool into the examples/ directory, that would be really helpful. If you add a system test to make sure that your example is working, that would be awesome!
If you're considering adding new variables or features to this plugin, we'll assume you know a bit about development, and will simply outline the process to you. Please read the plugin's rationale to check if the feature you're thinking of proposing looks like a good fit.
Please:
- Install Dagger
- If you'd prefer Dagger not to use Docker to bootstrap BuildKit for you,
but instead you've run BuildKit manually, simply teach Dagger about your
Buildkit server with
BUILDKIT_HOST=
- If you'd prefer Dagger not to use Docker to bootstrap BuildKit for you,
but instead you've run BuildKit manually, simply teach Dagger about your
Buildkit server with
- Fork the repo and check out (or branch off) the
develop
branch - Make changes, including adding tests
- Run
dagger do test
- Make both your tests and the existing tests pass
- Push your changes to your fork
- Open a PR to incoporate your changes into the
develop
branch
I got bored with the repeated process of having to check that ASDF plugins
weren't malicious, so I wrote a plugin to replace ~90% of the ASDF plugins that
I use. This is that plugin: asdf-ace
. If you'd like more detail, read the
:accordion:.
πͺ Why oh why oh why?
ASDF is great! Just look at the long list of all the plugins folks have written for it, and the size of its ecosystem! The folks who bother to take the time to write those plugins are excellent human beings - their efforts helped me start using ASDF, and I thank them for all their hard work :-)
I started using ASDF a while ago, as I wanted to replace a DIY version management system I'd hacked into existence. But as I added to the set of tools that I managed via ASDF, a boring process kept happening.
For every tool I installed, I found myself having to go into the associated plugin's repo and double-check that its installer did what it said. After all, it's running code on my machine ... I need to check it's not malicious.
I began to notice that each of the tools I installed, using plugins in that long list, seemed to belong to one of three categories:
- tools that are complex to install, and perhaps bring their own package
manager along with them (e.g.
ruby
,node
,npm
) - tools that require several steps to compile, install; or perhaps unpack and arrange many files from an installer on the target system
- tools whose installers download a single file from the internet, maybe uncompress it, and then chmod+x the result.
Over time, a pattern emerged.
I noticed that a significant majority of the tools I installed belonged to
category #3: download
, then maybe extract
, then chmod
. And very often
their plugins were copy/paste versions of some normalised ASDF shell script ...
but I still had to manually check each one, as I couldn't be sure they
actually were a copy/paste.
This commonality strongly suggested that there was a repeatable, automatable solution to this problem. And so, because I wanted to stop repeatedly performing this mini-audit, over and over again, I wrote this plugin.
This plugin, asdf-ace
tries to handle the simple, boring, "download a binary
off the internet and chmod it" case for tools that fit into category #3.
It takes a tiny bit of setup and 1% more effort to use than the many-plugins approach, but it's worth it to me.
By publishing config examples that users can cut'n'paste, even that tiny bit of setup and +1% effort is reduced.
By allowing users to put anything they like in their configuration file, this plugin's repo doesn't act as a gatekeeper of which tools are "allowed" to be installed with this plugin. Adding any specific tool's config into the examples directory is merely a friendly, positive addition to the ecosystem, and not a required step in the process of using this plugin to install that tool.
Hopefully, if you also like to know what code is running on your machine, you don't have to trust this plugin: you can audit the code in this repo once, pin to a specific commit for all your boring-tool ASDF installs, and not have to repeat the audit rigamarole of a subtly different plugin for each new tool you teach it about. Yes, you'll miss out on new features that this plugin introduces (remote version listing and binary checksum checking is planned!), but you'll know that you understand what's running on your machine.
To understand the plugin's inner workings, please read the technical details section.
This plugin:
- learns which tool is it being told to install, by looking at the directory hierarchy into which it has been git-cloned by ASDF
- learns which version of the tool it is being told to install, by looking at an environment variable set by ASDF
- discovers various runtime-specific facts about the machine it's running on (e.g. processor architecture; OS type)
- feeds all the above data points into a CUE-based templating system, which ...
- reads the user-specified configuration, which contains user-authored mappings from facts to tool-and-version-specific download and install parameters; and
- emits a pair of download and install snippets, tailored to the machine on which the tool is being installed.
The plugin then:
- executes the download snippet to fetch the remote file
- executes the install snippet, which involves it:
- uncompressing the downloaded file, if the user indicates that the remote
file is a
.zip
or.tar.gz
- renaming the resulting file to the name the user chose
- making the file executable
- moving the executable into the directory that ASDF told it to use
- uncompressing the downloaded file, if the user indicates that the remote
file is a
If the user points to a remote .zip
or .tar.gz
, the tool can be told to
extract more than one executable binary from the archive. This mechanism is
deliberately intended not to scale beyond a few files.
This plugin is a more capable but more complicated version of the following (fake, non-functional) shell script. It's hidden behind a πͺ because it's so ugly that it's offputting, and this shell script plays absolutely no part in this plugin's job: it's shown here solely as a pseudo-code analogy!
πͺ Don't worry - this isn't the actual implementation!
config_file=~/.config/asdf-ace/tools.not-really.this-is-fake.txt
read TOOL_TO_INSTALL
read VERSION_TO_INSTALL
read INSTALL_DIRECTORY
URL=$(grep "^${TOOL_TO_INSTALL}.url" $config_file | cut -f2)
UNAME_M=$(uname -m)
UNAME_S=$(uname -s)
cat generic-download-script \
| sed -e "s/__URL__/$URL/g" \
-e "s/__VERSION__/$VERSION_TO_INSTALL/g" \
-e "s/__UNAME_M__/$UNAME_M/g" \
-e "s/__UNAME_S__/$UNAME_S/g" \
| bash \
> downloaded-file
UNPACKER=$(grep "^${TOOL_TO_INSTALL}.unpacker" $config_file | cut -f2)
BINARY=$(grep "^${TOOL_TO_INSTALL}.binary" $config_file | cut -f2)
cat generic-install-script \
| sed -e "s/__UNPACKER__/$UNPACKER/g" \
-e "s/__BINARY__/$BINARY/g" \
-e "s/__DOWNLOAD__/downloaded-file/g" \
-e "s/__VERSION__/$VERSION_TO_INSTALL/g" \
-e "s/__INSTALL_TO__/$INSTALL_DIRECTORY/g" \
-e "s/__UNAME_M__/$UNAME_M/g" \
-e "s/__UNAME_S__/$UNAME_S/g" \
| bash
More detail will be added here.