Source code for meta_package_manager.managers.luarocks

# 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 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. ```{note} Every read passes `--porcelain`, which replaces the default grouped display with one tab-separated record per line. ``` ```{note} 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. ``` ```{caution} `--no-project` is load-bearing. 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 {attr}`pre_args` rather than repeated per operation. The version probe skips `pre_args` entirely and so runs bare. ``` ```{caution} Reads and writes disagree about scope, and deliberately so. `list` reports every configured tree at once, which is the right answer for an inventory of the machine: the system tree and the user's `~/.luarocks` both show up, each row naming the tree holding it. `install` and `remove` act on one tree only, the default one, so removing a rock that lives in the *other* tree fails with `Error: Could not find rock 'say' in /opt/homebrew`. That asymmetry is left as LuaRocks defines it rather than papered over. 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. ``` ```{note} No `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. No `sync` either, nothing refreshing the manifest without also downloading, and no `cleanup`: `luarocks purge` empties an entire tree rather than reclaiming anything, which is a mass removal and not a cleanup. ``` Documentation: [LuaRocks documentation](https://github.com/luarocks/luarocks/wiki/Documentation). """ name = "LuaRocks" homepage_url = "https://luarocks.org" 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, ...]: """Generate 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)