Source code for meta_package_manager.managers.lure

# 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 functools import cached_property

from extra_platforms import LINUX_LIKE

from ..capabilities import search_capabilities, version_not_implemented
from ..execution import VERSION_PROBE
from ..manager import PackageManager
from ..version import parse_version

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

    from ..package import Package
    from ..version import TokenizedString


[docs] class Lure(PackageManager): """LURE, the Linux User REpository: an AUR-shaped build system for any distro. A package is a build recipe in a git repository LURE clones, and the resulting artifact is handed to the host's own package manager, so LURE reaches packages no distro archive carries while leaving the installed state with `apt`, `dnf`, `pacman` or whichever backend the host runs. ```{important} LURE escalates itself and refuses to be escalated. It aborts as root with *"Running LURE as root is forbidden as it may cause catastrophic damage to your system"*, and calls its own configured `RootCmd` (`sudo` by default) for the privileged half. mpm therefore marks nothing here privileged, the same treatment [`chromebrew`](chromebrew.md) gets. ``` ```{caution} `remove` and `upgrade` are absent, and the reason is one field. LURE's default backend options set `NoConfirm: false`, and only its build path flips it true, so an install reaches the backend as `apt install -y` while a removal reaches it as a bare `apt remove` and blocks forever on a confirmation prompt no mpm run can answer. `--pm-args` can push a flag through, but the right flag is the backend's own: `-y` suits apt, dnf and zypper, while pacman reads `-y` as *refresh the databases*, so there is no value mpm could pass without knowing a backend it cannot resolve. ``` ```{note} Every subcommand pulls its repositories before doing anything else, so even a listing costs a network round-trip and prints the git progress on `<stderr>`. Only the packages themselves reach `<stdout>`. ``` Documentation: [LURE readme](https://github.com/lure-sh/lure). """ maintenance_note: str | None = ( "LURE's own forge at `git.elara.ws` answers `502` and `lure.sh` answers " "`404` behind a placeholder certificate, so its installer and release " "binaries are both unreachable and the tool has to be built from source. " "Its author announced a pause rather than an end in " "[lure-sh/lure#89](https://github.com/lure-sh/lure/issues/89) on " "2025-05-04, *\"I intend to start working on it again as soon as I'm in a " 'stable position again"*, and the recipe repository this manager reads ' "is on GitHub and still moving." ) name = "LURE" homepage_url = "https://github.com/lure-sh/lure" platforms = LINUX_LIKE requirement = ">=0.1.3" """The newest release, and the one this parser was checked against: `list.go` already carries the `installed` flag there. The build driven here is that tag plus a single commit touching the install script, LURE having tagged nothing since. """ cli_names = ("lure",) _LIST_REGEXP = re.compile( r"^[^/\s]+/(?P<package_id>\S+)\s+(?P<version>\S+)$", ) """A row is `<repo>/<name> <version>`. The repository is matched but dropped: `lure install` takes the bare name, so that is what a package ID has to be for mpm to hand one back. Two repositories shipping the same name would collapse onto one entry, which in practice is a single `default` repository. """ version_cli_options = ("version",) """`lure --version` is rejected as `flag provided but not defined`, the version living behind a subcommand instead. """ version_regexes = (r"v?(?P<version>\d+\.\d+\.\d+)",) """ ```{code-block} shell-session $ lure version v0.1.3 ``` """
[docs] @cached_property def version(self) -> TokenizedString | None: """Parse the version off `<stderr>`, where LURE alone prints it. `lure version` reaches Go's builtin `println`, which writes to `<stderr>` and leaves `<stdout>` empty, so the inherited probe (reading the return value of {meth}`~meta_package_manager.execution.CLIExecutor.run_cli`) would find nothing and leave the manager permanently unavailable. Both streams are searched, not `<stderr>` alone: a version that moved to `<stdout>` should not take the manager offline. """ if not self.executable: return None self._active_operation = VERSION_PROBE output = self.run_cli( self.version_cli_options, auto_pre_cmds=False, auto_pre_args=False, auto_post_args=False, force_exec=True, ) error = self._last_run[2] if self._last_run else "" haystack = "\n".join(stream for stream in (output, error) if stream) for regex in self.version_regexes: match = re.compile(regex, re.MULTILINE).search(haystack) if match: version_string = match.groupdict().get("version") if version_string: return parse_version(version_string) return None
def _parse_rows(self, output: str) -> Iterator[tuple[str, str]]: """Yield `(package_id, version)` per row of a `list` output. The two listings share one row shape but not one meaning, the version being the installed one under `--installed` and the available one otherwise, so the rows are read here and labelled by each caller rather than through {meth}`~meta_package_manager.manager.PackageManager.parse_regex_lines`. """ for line in output.splitlines(): match = self._LIST_REGEXP.match(line) if match: yield match.group("package_id"), match.group("version") @property def installed(self) -> Iterator[Package]: """Fetch installed packages. The version is the built artifact's, carrying the recipe's release number, where {meth}`search` reports the recipe's own. ```{code-block} shell-session $ lure list --installed default/neofetch 7.1.0-1 ``` """ output = self.run_cli("list", "--installed") for package_id, version in self._parse_rows(output): yield self.package(id=package_id, installed_version=version)
[docs] @search_capabilities(extended_support=False, exact_support=False) def search(self, query: str, extended: bool, exact: bool) -> Iterator[Package]: """Fetch matching packages. LURE has no search verb: `list` prints the whole catalog of every repository it tracks, and mpm's own refiltering narrows it. ```{caution} Search does not support extended or exact matching, and the query never reaches LURE. ``` ```{code-block} shell-session $ lure list default/admc 0.13.0-alt1 default/admc-git 3592.023670c default/binutils-z80 2.43 default/cava 0.9.1 default/cava-git 0 default/deduplicator 0.1.6 ``` """ output = self.run_cli("list") for package_id, version in self._parse_rows(output): yield self.package(id=package_id, latest_version=version)
[docs] @version_not_implemented def install(self, package_id: str, version: str | None = None) -> str: """Install one package, building it from its recipe. This is the one path LURE runs non-interactively on its own: the build sets `NoConfirm`, so the backend is handed its own confirmation flag. ```{code-block} shell-session $ lure install neofetch ``` """ return self.run_cli("install", package_id)
[docs] def sync(self) -> None: """Pull every tracked recipe repository. ```{code-block} shell-session $ lure refresh ``` """ self.run_cli("refresh")