# 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 typing import ClassVar
from extra_platforms import ALL_PLATFORMS
from ..manager import PackageManager
from ..version import parse_version
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Iterator
from ..package import Package
[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 the `rustup` definition resolves the same question, by narrowing
# to what the listing can enumerate.
#
# `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.
#
# `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 `installed`
# reports.
operation_notes: ClassVar = {
"remove": (
"The `ros delete` command reports success and breaks roswell's "
"own runtime, so `mpm` will not drive it."
),
"upgrade": (
"The `ros update` command upgrades Quicklisp systems, never the "
"implementations the listing reports."
),
}
name = "Roswell"
homepage_url = "https://roswell.github.io/"
documentation_url = "https://github.com/roswell/roswell/wiki"
repository_url = "https://github.com/roswell/roswell"
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)
```
"""
version_from_stderr = True
"""Both `ros --version` and `ros version` leave `<stdout>` empty and write to
`<stderr>`, measured at 0 and 35 bytes respectively."""
@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}`~meta_package_manager.manager.PackageManager.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,
)