Source code for meta_package_manager.managers.luarocks

# Copyright Kevin Deldycke <[email protected]> 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 typing import ClassVar

from extra_platforms import ALL_PLATFORMS

from ..capabilities import search_capabilities
from ..manager import PackageManager
from ..version import parse_version

TYPE_CHECKING = False
if TYPE_CHECKING:
    from collections.abc import Iterable, Iterator

    from ..package import Package


[docs] class LuaRocks(PackageManager): """LuaRocks, the package manager for Lua modules. A package is a *rock*, identified by the bare name every listing prints. Its version carries a packaging revision after a dash (`3.1.2-0`), kept verbatim because that is the form `luarocks install` accepts back. ```{caution} The listing reports every configured tree at once, while `install` and `remove` act on the default tree only: removing a rock that lives in another tree fails and names the tree it searched. ``` """ # Every read passes `--porcelain`, which replaces the default grouped # display with one tab-separated record per line. # # Search is what makes this a class rather than a bundled definition: # `luarocks search` prints one row per *(rock, version, kind)* triple, so # a single rock comes back a dozen times over: once per published # version, and again for each of the `rockspec` and `src` forms it ships # in. mpm keys a package on its id alone, so the rows are reduced here to # one entry per rock. The installed and outdated listings get the same # treatment, LuaRocks being able to hold several versions of a rock in # one tree. # # `--no-project` is required. LuaRocks walks up from the working # directory looking for a project tree and silently switches to it when # one is found, so without the flag the inventory would answer for # whichever directory mpm happened to be invoked from instead of for the # machine. `haxelib` forces `--global` against the same hazard. The flag # is a global one, placed before the subcommand, which is why it is # declared as `pre_args` rather than repeated per operation. The version # probe skips `pre_args` entirely and so runs bare. # # Reads and writes disagree about scope, and deliberately so: `list` # reporting every configured tree at once is the right answer for an # inventory of the machine, while `install` and `remove` acting on the # default tree only is left as LuaRocks defines it. Forcing `--local` # would make the user tree writable at the cost of the system one, and # forcing `--global` the reverse; neither is right for every host, and # the failure is loud, immediate and names the tree it searched. # # `upgrade --all`: LuaRocks has no command updating every installed rock, # `install` being what upgrades a named one in place. mpm backfills the # bulk case from `outdated` plus the per-rock upgrade (rendered by the # synthesized note of the operations table). # # `sync`: nothing refreshes the manifest without also downloading. # # `cleanup`: `luarocks purge` empties an entire tree rather than # reclaiming anything, which is a mass removal and not a cleanup. operation_notes: ClassVar = { "installed": ( "A rock can be held at several versions in one tree; the listing " "reports one entry per rock." ), "search": ( "Results are reduced to one entry per rock, the tool printing one " "row per version and per form." ), "sync": "Nothing refreshes the manifest without also downloading.", "cleanup": ( "The `purge` command empties an entire tree rather than " "reclaiming anything." ), } name = "LuaRocks" homepage_url = "https://luarocks.org" documentation_url = "https://github.com/luarocks/luarocks/wiki/Documentation" repository_url = "https://github.com/luarocks/luarocks" wikipedia_url = "https://en.wikipedia.org/wiki/LuaRocks" platforms = ALL_PLATFORMS requirement = ">=3.9.1" """The release adding `--no-project`, which is what pins every read to the machine instead of to the working directory's project tree. """ pre_args = ("--no-project",) """Ignore any project tree found by walking up from the working directory, so the inventory describes the machine and not the current directory. """ version_regexes = (r"luarocks(?:\.exe)?[ \t]+(?P<version>\d+\.\d+(?:\.\d+)?)",) r"""Search the version right after the binary path LuaRocks echoes back. ```{code-block} shell-session $ luarocks --version /opt/homebrew/bin/luarocks 3.13.0 LuaRocks main command-line interface ``` The path is printed in full and varies per host, so the pattern anchors on the binary name rather than on the start of the line. """ _INSTALLED_REGEXP = re.compile( r"^(?P<package_id>[^\t]+)\t(?P<installed_version>[^\t]+)\tinstalled\t", ) """One installed rock: name, version, the literal `installed` state, then the tree holding it. The state column is matched so a row of any other kind is skipped rather than misread. """ _OUTDATED_REGEXP = re.compile( r"^(?P<package_id>[^\t]+)\t(?P<installed_version>[^\t]+)\t" r"(?P<latest_version>[^\t]+)\t", ) """One upgradable rock: name, the version held, the version available, then the server offering it. """ _SEARCH_REGEXP = re.compile( r"^(?P<package_id>[^\t]+)\t(?P<latest_version>[^\t]+)\t", ) """One search hit: name and version. The kind and server columns are dropped, being what makes the same rock repeat across rows. """ def _newest_per_rock( self, rows: Iterable[tuple[str, str | None, str | None]], ) -> Iterator[Package]: """Reduce rows to one package per rock, keeping the highest version. LuaRocks repeats a rock across rows, by installed version in a listing and by published version and packaging kind in a search. Ranking is done on whichever version the row carries: the installed one where there is one, the available one otherwise. """ best: dict[str, tuple[str | None, str | None]] = {} for package_id, installed, latest in rows: ranked = installed or latest current = best.get(package_id) if current is not None: current_ranked = current[0] or current[1] if ranked is None or ( current_ranked is not None and parse_version(ranked) <= parse_version(current_ranked) ): continue best[package_id] = (installed, latest) for package_id, (installed, latest) in best.items(): yield self.package( id=package_id, installed_version=installed, latest_version=latest, ) @property def installed(self) -> Iterator[Package]: """Fetch installed packages. ```{code-block} shell-session $ luarocks --no-project list --porcelain inspect 3.1.2-0 installed /Users/kde/.luarocks/lib/luarocks/rocks-5.5 say 1.4.1-3 installed /Users/kde/.luarocks/lib/luarocks/rocks-5.5 ``` """ output = self.run_cli("list", "--porcelain") yield from self._newest_per_rock( (match.group("package_id"), match.group("installed_version"), None) for match in map(self._INSTALLED_REGEXP.match, output.splitlines()) if match ) @property def outdated(self) -> Iterator[Package]: """Fetch outdated packages. ```{code-block} shell-session $ luarocks --no-project list --outdated --porcelain inspect 3.1.2-0 3.1.3-0 https://luarocks.org ``` """ output = self.run_cli("list", "--outdated", "--porcelain") yield from self._newest_per_rock( ( match.group("package_id"), match.group("installed_version"), match.group("latest_version"), ) for match in map(self._OUTDATED_REGEXP.match, output.splitlines()) if match )
[docs] @search_capabilities(extended_support=False, exact_support=False) def search(self, query: str, extended: bool, exact: bool) -> Iterator[Package]: """Fetch matching packages. ```{caution} Search does not support extended or exact matching. ``` ```{code-block} shell-session $ luarocks --no-project search say --porcelain say 1.4.1-3 rockspec https://luarocks.org say 1.4.1-3 src https://luarocks.org say 1.4.0-1 rockspec https://luarocks.org say 1.4.0-1 src https://luarocks.org say 1.3-1 rockspec https://luarocks.org ``` """ output = self.run_cli("search", query, "--porcelain") yield from self._newest_per_rock( (match.group("package_id"), None, match.group("latest_version")) for match in map(self._SEARCH_REGEXP.match, output.splitlines()) if match )
[docs] def install(self, package_id: str, version: str | None = None) -> str: """Install one package. ```{code-block} shell-session $ luarocks --no-project install inspect inspect 3.1.3-0 depends on lua >= 5.1 (5.5-1 provided by VM: success) No existing manifest. Attempting to rebuild... inspect 3.1.3-0 is now installed in /opt/homebrew (license: MIT <http://opensource.org/licenses/MIT>) ``` """ spec = (package_id,) if version is None else (package_id, version) return self.run_cli("install", *spec)
[docs] def upgrade_one_cli( self, package_id: str, version: str | None = None, ) -> tuple[str, ...]: """Generates the CLI to upgrade one package. LuaRocks has no `upgrade` verb: installing a rock that is already present replaces it with the newer build, which is the upgrade. ```{code-block} shell-session $ luarocks --no-project install inspect inspect 3.1.3-0 depends on lua >= 5.1 (5.5-1 provided by VM: success) No existing manifest. Attempting to rebuild... inspect 3.1.3-0 is now installed in /opt/homebrew (license: MIT <http://opensource.org/licenses/MIT>) ``` """ spec = (package_id,) if version is None else (package_id, version) return self.build_cli("install", *spec)
[docs] def remove(self, package_id: str) -> str: """Remove one package. Acts on the default tree, so a rock held in another one is refused by name rather than removed, as covered in the class notes above. ```{code-block} shell-session $ luarocks --no-project remove inspect Checking stability of dependencies in the absence of inspect 3.1.3-0... Removing inspect 3.1.3-0... Removal successful. ``` """ return self.run_cli("remove", package_id)