Source code for meta_package_manager.managers.roswell

# 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 ALL_PLATFORMS

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 Roswell(PackageManager): """Roswell, a Common Lisp implementation installer and launcher. ```{important} A roswell package is a **Lisp implementation**, not a Lisp library. `ros install` accepts both an implementation and a Quicklisp system, but `ros list installed` answers for implementations alone: a system installed with `ros install cl-ppcre` never appears there. The two verbs disagree on their object, and this wrap resolves it the way {class}`~meta_package_manager.managers.rustup.RustUp` resolves the same question, by narrowing to what the listing can enumerate. ``` ```{caution} `remove` is deliberately absent. `ros delete <impl>` reports success on every channel that matters, exiting `0` with `sbcl-bin was deleted successfully.` and an empty `<stderr>`, and leaves roswell unusable: its own runtime core was built against the implementation just deleted, so every later subcommand answers `<impl>/<version> does not exist.stop.`. Recovery means deleting `~/.roswell` and running `ros setup` again, `ros config set` being just as broken as the rest. mpm will not drive a removal that reports success and breaks the tool. ``` ```{caution} `upgrade` is absent for the same reason `remove` is, one level up: `ros update` resolves its argument through `asdf:find-system`, so it upgrades Quicklisp systems, never the implementations {meth}`installed` reports. Mapping it here would upgrade something other than what mpm just listed. ``` Documentation: [Roswell wiki](https://github.com/roswell/roswell/wiki). """ name = "Roswell" homepage_url = "https://roswell.github.io/" logo = "commonlisp" keywords = ("common lisp", "quicklisp") platforms = ALL_PLATFORMS requirement = ">=22.12.14.113" """The oldest release whose listing this parser was checked against. `lisp/list-installed.lisp` is byte-identical from there through the current `26.02.116`, the `*error-output*` header included, so the shape below is not a property of the release that happened to be driven. """ cli_names = ("ros",) _INSTALLED_REGEXP = re.compile( r"^(?P<package_id>[^/\s]+)/(?P<installed_version>\S+)$", ) version_regexes = (r"roswell\s+(?P<version>[^\s(]+)",) """The trailing `(NO-GIT-REVISION)` is left out of the capture: it names the build, not the version, and a release tarball carries it in place of a commit. ```{code-block} shell-session $ ros --version roswell 26.02.116(NO-GIT-REVISION) ``` """
[docs] @cached_property def version(self) -> TokenizedString | None: """Parse the version off `<stderr>`, where roswell alone prints it. Both `ros --version` and `ros version` leave `<stdout>` empty and write to `<stderr>`, measured at 0 and 35 bytes respectively. The inherited probe reads the return value of {meth}`~meta_package_manager.execution.CLIExecutor.run_cli`, which is `<stdout>`, so it would find nothing and leave the manager permanently unavailable. The streams are reached through {attr}`~meta_package_manager.execution.CLIExecutor._last_run` instead, the same attribute `bin` and `gext` read for their own `<stderr>`-reporting operations. """ if not self.executable: return None # Matches the inherited probe: a version check is a read-only liveness # test, so it takes the short timeout rather than the mutating one. 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, ) # Both streams are searched rather than `<stderr>` alone: roswell prints # there today, and a version that moved to `<stdout>` would otherwise # take the manager offline for no reason. 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
@property def installed(self) -> Iterator[Package]: """Fetch installed Lisp implementations. Only the rows reach `<stdout>`; roswell prints its headings to `<stderr>`, so the payload arrives with no furniture to skip. An implementation is listed once per installed version, so the rows are reduced to the newest version of each. ```{caution} An empty listing is not proof of an empty machine. Roswell answers `0` with nothing on `<stdout>` and its diagnosis on `<stderr>` alone once it can no longer resolve the implementation it runs on, which a `ros delete` of the wrong version is enough to cause. `must_succeed` cannot catch that, keying on the exit code, so the two streams are compared instead and a silent failure is raised rather than reported as zero packages. ``` ```{code-block} shell-session $ ros list installed sbcl-bin/2.6.8 ``` """ output = self.run_cli("list", "installed") if not output.strip(): _code, _stdout, error = self._last_run or (0, "", "") if error.strip(): msg = ( "Roswell reported no installed implementation while writing " f"to <stderr>, so the listing is unreliable: {error.strip()}" ) raise RuntimeError(msg) newest: dict[str, str] = {} for match in map(self._INSTALLED_REGEXP.match, output.splitlines()): if not match: continue package_id = match.group("package_id") installed_version = match.group("installed_version") current = newest.get(package_id) if current is not None and parse_version( installed_version, ) <= parse_version(current): continue newest[package_id] = installed_version for package_id, installed_version in newest.items(): yield self.package(id=package_id, installed_version=installed_version)
[docs] def install(self, package_id: str, version: str | None = None) -> str: """Install one Lisp implementation. A version is appended to the implementation with a slash, the same `impl/version` spelling {meth}`installed` reports. ```{code-block} shell-session $ ros install sbcl-bin ``` ```{code-block} shell-session $ ros install sbcl-bin/2.6.8 ``` """ return self.run_cli( "install", f"{package_id}/{version}" if version else package_id, )