Skip to content

Commit

Permalink
feat: Enable use of latest: in .tool-versions files
Browse files Browse the repository at this point in the history
This change enables `asdf`'s existing latest-version-resolution
functionality within the `.tool-versions` file itself. Rather than
having to have a `.tool-versions` file that contains a full version
number:

```
java corretto-21.0.5.11.1
```

...you can now use the same `latest:` syntax that is already available
in the `local` & `global` commands, ie:

```
java latest:corretto-21
```

### Use case

For many tool/runtime ecosystems (eg Java), if a program runs correctly under
a specific version of that runtime, it can generally be relied on to run
correctly under any _later_ version of that runtime with the same major version
number (eg if a project runs under Corretto Java 21.0.5.11.1, it will run on
any _later_ version of Corretto Java 21).

This means that for projects in those ecosystems, there is little incentive
to pin to fully-specified versions like `21.0.5.11.1`, and in fact there are
downsides - over time, developers will default to using older, unpatched versions
of Java, unless they are assiduous in continually updating the contents of
the `.tool-versions` file, or have tooling devoted to doing so.

At the Guardian we have several hundred projects that run on the Java platform,
and due to our security obligations we generally want to be running under the
_latest_ security-patched version of the Java runtime that matches our
major-version requirement. We love `asdf` as a tool, and like that the
`.tool-versions` file can become a source-of-truth documenting which version
of Java a project uses, but we don't want to have to commit fully-specified
version numbers like `21.0.5.11.1` to source control, or set up tooling to
increment those version numbers across those hundreds of repositories.

Allowing the use of `latest:` in the `.tool-versions` file means that we
don't need to continually update those `.tool-versions` files. It also
partially addresses some of the needs raised by asdf-vm#1736,
though this solution uses the existing `asdf` version-resolution functionality,
rather than adopting the version requirements system used in nodejs.

### Implementation

A new `resolve_version_spec()` function has been extracted from the
existing `version_command()` function. This takes a version-spec string,
like `latest:corretto-11` or `corretto-21.0.5.11.1`, and resolves it to
a definite installed version number (if the resolved version is not
installed, the appropriate error message is shown).

This new `resolve_version_spec()` function is now also called in
`select_version()`, used by `with_shim_executable()`, meaning that any
execution of the `asdf` shim (eg, executing `java`) will now resolve
any version specifications found in the `.tool-versions` file - if
`.tool-versions` contains `java latest:corretto-21`, this will be
resolved and the latest version of Java 21 used.

## Other Information

Previous `asdf` PRs relating to `latest`:

* asdf-vm#575 in November 2019: added the `latest`
  command, eg `asdf latest python 3.6` reports the latest version of Python 3.6.
* asdf-vm#633 in July 2021: made it possible
  to specify `latest` when using the `local` & `global` commands, eg:
  `asdf local python latest:3.7` - this would save a precise version number
  to `.tools-versions`, which is undesired behaviour for us at the Guardian.

A couple of Guardian systems attempting to standardise on using `.tool-versions`
as a source of truth:

* guardian/gha-scala-library-release-workflow#36
* https://github.com/guardian/setup-scala
  • Loading branch information
rtyley committed Oct 17, 2024
1 parent c5116dc commit 392cfa6
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 23 deletions.
2 changes: 2 additions & 0 deletions bin/asdf
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ fi

# shellcheck source=lib/utils.bash
. "$(dirname "$(dirname "$0")")/lib/utils.bash"
# shellcheck source=lib/functions/versions.bash
. "$(dirname "$(dirname "$0")")/lib/functions/versions.bash"

find_cmd() {
local cmd_dir="$1"
Expand Down
3 changes: 2 additions & 1 deletion lib/commands/command-current.bash
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ plugin_current_command() {
local description=""

IFS=' ' read -r -a versions <<<"$full_version"
for version in "${versions[@]}"; do
for version_spec in "${versions[@]}"; do
version="$(resolve_version_spec "$version_spec")"
if ! (check_if_version_exists "$plugin_name" "$version"); then
version_not_installed="$version"
fi
Expand Down
48 changes: 27 additions & 21 deletions lib/functions/versions.bash
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,7 @@ version_command() {
declare -a resolved_versions
local item
for item in "${!versions[@]}"; do
IFS=':' read -r -a version_info <<<"${versions[$item]}"
if [ "${version_info[0]}" = "latest" ] && [ -n "${version_info[1]}" ]; then
version=$(latest_command "$plugin_name" "${version_info[1]}")
elif [ "${version_info[0]}" = "latest" ] && [ -z "${version_info[1]}" ]; then
version=$(latest_command "$plugin_name")
else
# if branch handles ref: || path: || normal versions
version="${versions[$item]}"
fi

# check_if_version_exists should probably handle if either param is empty string
if [ -z "$version" ]; then
exit 1
fi

if ! (check_if_version_exists "$plugin_name" "$version"); then
version_not_installed_text "$plugin_name" "$version" 1>&2
exit 1
fi

resolved_versions+=("$version")
resolved_versions+=("$(resolve_version_spec "${versions[$item]}")")
done

if [ -f "$file" ] && grep -q "^$plugin_name " "$file"; then
Expand All @@ -79,6 +59,32 @@ version_command() {
fi
}

resolve_version_spec() {
local version_spec=$1

IFS=':' read -r -a version_info <<<"$version_spec"
if [ "${version_info[0]}" = "latest" ] && [ -n "${version_info[1]}" ]; then
version=$(latest_command "$plugin_name" "${version_info[1]}")
elif [ "${version_info[0]}" = "latest" ] && [ -z "${version_info[1]}" ]; then
version=$(latest_command "$plugin_name")
else
# if branch handles ref: || path: || normal versions
version="${versions[$item]}"
fi

# check_if_version_exists should probably handle if either param is empty string
if [ -z "$version" ]; then
exit 1
fi

if ! (check_if_version_exists "$plugin_name" "$version"); then
version_not_installed_text "$plugin_name" "$version" 1>&2
exit 1
fi

printf "%s\n" "$version"
}

list_all_command() {
local plugin_name=$1
local query=$2
Expand Down
3 changes: 2 additions & 1 deletion lib/utils.bash
Original file line number Diff line number Diff line change
Expand Up @@ -736,7 +736,8 @@ select_version() {
version_and_path=$(find_versions "$plugin_name" "$search_path")
IFS='|' read -r version_string _path <<<"$version_and_path"
IFS=' ' read -r -a usable_plugin_versions <<<"$version_string"
for plugin_version in "${usable_plugin_versions[@]}"; do
for version_spec in "${usable_plugin_versions[@]}"; do
plugin_version="$(resolve_version_spec "$version_spec")"
for plugin_and_version in "${shim_versions[@]}"; do
local plugin_shim_name
local plugin_shim_version
Expand Down
12 changes: 12 additions & 0 deletions test/current_command.bats
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ teardown() {
[ "$output" = "$expected" ]
}

@test "current should not error on version specifications like 'latest:'" {
cd "$PROJECT_DIR"
echo "dummy 1.2.0 latest:1.1" >>"$PROJECT_DIR/.tool-versions"
expected="dummy 1.2.0 latest:1.1 $PROJECT_DIR/.tool-versions"

asdf current "dummy"
echo "$expected"
run asdf current "dummy"
[ "$status" -eq 0 ]
[ "$output" = "$expected" ]
}

@test "current should derive from the legacy file if enabled" {
cd "$PROJECT_DIR"
echo 'legacy_version_file = yes' >"$HOME/.asdfrc"
Expand Down
9 changes: 9 additions & 0 deletions test/install_command.bats
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ teardown() {
[ "$(cat "$ASDF_DIR/installs/dummy/1.2.0/version")" = "1.2.0" ]
}

@test "install_command installs the 'latest:' version in .tool-versions" {
cd "$PROJECT_DIR"
echo -n 'dummy latest:1.1' >".tool-versions"
run asdf install dummy
ls "$ASDF_DIR/installs/dummy/"
[ "$status" -eq 0 ]
[ "$(cat "$ASDF_DIR/installs/dummy/1.1.0/version")" = "1.1.0" ]
}

@test "install_command set ASDF_CONCURRENCY" {
run asdf install dummy 1.0.0
[ "$status" -eq 0 ]
Expand Down

0 comments on commit 392cfa6

Please sign in to comment.