# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
from __future__ import annotations
import re
from extra_platforms import LINUX_LIKE, MACOS
from ..capabilities import search_capabilities, version_not_implemented
from ..manager import PackageManager
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Iterator
from ..package import Package
[docs]
class ASDF(PackageManager):
"""asdf manages parallel versions of multiple developer tools (Node.js,
Ruby, Python, Erlang, ...) through a plugin ecosystem, exposing all of
them behind a single CLI.
.. note::
asdf is plugin-driven: every tool the user can install is gated
behind a plugin (``asdf plugin add nodejs``). ``mpm install`` does
not auto-add plugins; the user is expected to register them first
with ``asdf plugin add``.
.. note::
Each ``(plugin, installed_version)`` pair is reported as a
distinct package, so a tool installed at multiple versions yields
multiple entries sharing the same ID.
.. caution::
``mpm outdated`` only reports tools that have a currently-active
version (marked with ``*`` in ``asdf list``) different from their
latest stable release. A tool installed without being activated
through a ``.tool-versions`` file does not surface as outdated.
"""
name = "asdf"
homepage_url = "https://asdf-vm.com"
platforms = LINUX_LIKE, MACOS
requirement = ">=0.16.0"
"""The Go rewrite shipped in ``0.16.0`` on 2025-01-30 replaced the
hyphenated subcommands (``asdf list-all``, ``asdf plugin-add``, ...)
with their space-separated equivalents this wrapper depends on
(``asdf list all``, ``asdf plugin add``, ...). Older Bash-based
releases are not supported.
"""
version_cli_options = ("version",)
version_regexes = (r"v?(?P<version>\d+\.\d+\.\d+)",)
"""
.. code-block:: shell-session
$ asdf version
v0.19.0-83adfe6
"""
_LATEST_REGEXP = re.compile(
r"^(?P<package_id>\S+)\s+(?P<latest_version>\S+)\s+(installed|missing)\s*$",
re.MULTILINE,
)
_PLUGIN_LIST_ALL_REGEXP = re.compile(
r"^(?P<package_id>\S+)(?:\s+(?P<description>.+?))?\s*$",
re.MULTILINE,
)
def _parse_list(self) -> Iterator[tuple[str, str, bool]]:
"""Parse ``asdf list`` output into ``(plugin, version, is_current)``
tuples.
Non-indented lines are plugin headers. Lines that start with two
spaces are installed-but-not-current versions; lines that start
with a space and ``*`` are the active version.
"""
output = self.run_cli("list")
current_plugin: str | None = None
for line in output.splitlines():
if not line:
continue
if not line.startswith(" "):
current_plugin = line.strip()
continue
if current_plugin is None:
continue
is_current = line.startswith(" *")
version = line.lstrip(" *").strip()
if version:
yield current_plugin, version, is_current
@property
def installed(self) -> Iterator[Package]:
"""Fetch installed packages.
Emits one :py:class:`meta_package_manager.package.Package` per
``(plugin, installed_version)`` pair, so a tool installed at
multiple versions yields multiple entries sharing the same ID.
.. code-block:: shell-session
$ asdf list
nodejs
18.20.4
*20.10.0
ruby
3.2.0
"""
for plugin, version, _ in self._parse_list():
yield self.package(id=plugin, installed_version=version)
@property
def outdated(self) -> Iterator[Package]:
"""Fetch outdated packages.
Cross-references the currently-active version per plugin (the
entry marked with ``*`` in ``asdf list``) with the latest stable
version (``asdf latest --all``). Only plugins whose active
version differs from the latest are yielded.
.. code-block:: shell-session
$ asdf latest --all
nodejs 20.10.0 missing
ruby 3.3.0 missing
"""
current_versions: dict[str, str] = {
plugin: version
for plugin, version, is_current in self._parse_list()
if is_current
}
latest_output = self.run_cli("latest", "--all")
for match in self._LATEST_REGEXP.finditer(latest_output):
plugin = match.group("package_id")
latest = match.group("latest_version")
installed = current_versions.get(plugin)
if installed and installed != latest:
yield self.package(
id=plugin,
installed_version=installed,
latest_version=latest,
)
[docs]
@search_capabilities(extended_support=False, exact_support=False)
def search(self, query: str, extended: bool, exact: bool) -> Iterator[Package]:
"""Fetch matching packages.
``asdf plugin list all`` enumerates the entire short-name plugin
catalogue. The framework's
:py:meth:`meta_package_manager.manager.PackageManager.refiltered_search`
narrows the listing down to entries that contain the query.
.. code-block:: shell-session
$ asdf plugin list all
1password-cli https://github.com/NeoHsu/asdf-1password-cli.git
act https://github.com/grimoh/asdf-act.git
nodejs https://github.com/asdf-vm/asdf-nodejs.git
"""
output = self.run_cli("plugin", "list", "all")
for match in self._PLUGIN_LIST_ALL_REGEXP.finditer(output):
yield self.package(
id=match.group("package_id"),
description=match.group("description"),
)
[docs]
def install(self, package_id: str, version: str | None = None) -> str:
"""Install one package.
``asdf install <plugin>`` requires the plugin to have been added
beforehand with ``asdf plugin add <plugin>``. This wrapper does
not auto-add plugins.
.. code-block:: shell-session
$ asdf install nodejs 20.10.0
"""
return self.run_cli("install", package_id, version or "latest")
[docs]
@version_not_implemented
def upgrade_one_cli(
self,
package_id: str,
version: str | None = None,
) -> tuple[str, ...]:
"""Generates the CLI to upgrade one package.
asdf has no native upgrade verb: installing the ``latest`` alias
downloads the newest stable release alongside any older versions
already on disk. The user is responsible for switching the
active version with ``asdf set`` if desired.
.. code-block:: shell-session
$ asdf install nodejs latest
"""
return self.build_cli("install", package_id, "latest")
[docs]
def remove(self, package_id: str) -> str:
"""Remove one package.
``asdf plugin remove`` deletes the plugin and every version of
the tool installed through it, which matches ``mpm``'s
"remove this package" contract more cleanly than iterating
``asdf uninstall`` per installed version.
.. code-block:: shell-session
$ asdf plugin remove nodejs
"""
return self.run_cli("plugin", "remove", package_id)
[docs]
def sync(self) -> None:
"""Refresh plugin metadata.
.. code-block:: shell-session
$ asdf plugin update --all
"""
self.run_cli("plugin", "update", "--all")