# 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 LINUX_LIKE
from ..capabilities import search_capabilities, version_not_implemented
from ..manager import PackageManager
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Iterator
from ..package import Package
[docs]
class Pacstall(PackageManager):
"""AUR-inspired, source-based package manager for Ubuntu and other Linux
distributions.
Pacstall builds packages from "pacscripts" and installs the results as
`.deb` archives through `dpkg`. `mpm` forces `NO_COLOR` and
`DISABLE_PROMPTS` so output is uncolored and every call stays
non-interactive.
pacstall self-escalates: every privileged subcommand re-execs the script
through `sudo pacstall` (its `elevate()` function), and its documented
invocation is unprefixed (`pacstall -I foo`). `mpm` therefore never
wraps it and marks it {attr}`internal_sudo
<meta_package_manager.execution.CLIExecutor.internal_sudo>` instead: a warm
`sudo` credential cache is kept alive for its mid-run re-exec, and the
hidden-prompt watchdog covers the cold-cache case.
```{note}
Listing installed packages is a two-step probe: piped `--list` prints
bare names with no versions, so `mpm` follows each with a
`--cache-info <pkg> version` call to recover its version.
```
```{note}
`--search` matches names only and reports no versions. There is no
per-package upgrade verb either, so upgrading one package reinstalls it
through `--install`.
```
"""
homepage_url = "https://pacstall.dev"
platforms = LINUX_LIKE
internal_sudo = True
requirement = ">=6.0.0"
extra_env: ClassVar = {"NO_COLOR": "1", "DISABLE_PROMPTS": "1"}
"""Suppress ANSI colors and disable interactive prompts."""
version_regexes = (r"(?P<version>\d+\.\d+\.\d+)",)
"""
```{code-block} shell-session
$ pacstall --version
6.3.7 Vanilla
```
"""
# Upgrade rows are indented (a tab in real pacstall output); the header line
# sits at column 0. Match any indentation, not a literal tab: Python
# formatters expand tabs inside the docstring sample the corpus test replays.
_OUTDATED_REGEXP = re.compile(
r"^[ \t]+(?P<package_id>\S+)\s+@\s+\S+"
r"\s+\(\s*(?P<installed_version>\S+)"
r"\s*->\s*(?P<latest_version>\S+)\s*\)",
re.MULTILINE,
)
_SEARCH_REGEXP = re.compile(
r"^(?P<package_id>\S+)\s+@\s+\S+$",
re.MULTILINE,
)
@property
def installed(self) -> Iterator[Package]:
"""Fetch installed packages.
```{code-block} shell-session
$ pacstall --list
neofetch
neovim
```
```{note}
When piped, `pacstall --list` outputs bare package names without
versions. A follow-up `pacstall --cache-info <pkg> version` call
retrieves the installed version per package.
```
"""
output = self.run_cli("--list")
for line in output.splitlines():
package_id = line.strip()
if not package_id:
continue
version_output = self.run_cli("--cache-info", package_id, "version")
installed_version = version_output.strip() if version_output else None
yield self.package(
id=package_id,
installed_version=installed_version,
)
@property
def outdated(self) -> Iterator[Package]:
"""Fetch outdated packages.
```{code-block} shell-session
$ pacstall --list-upgrades
Upgradable: 2
neofetch @ pacstall-programs#master ( 7.1.0-2 -> 7.2.0-1 )
neovim @ pacstall-programs#master ( 0.9.4-1 -> 0.10.0-1 )
```
"""
output = self.run_cli("--list-upgrades")
for match in self._OUTDATED_REGEXP.finditer(output):
yield self.package(
id=match.group("package_id"),
installed_version=match.group("installed_version"),
latest_version=match.group("latest_version"),
)
[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, and does not
provide version information. Returns the best subset of results and
lets
{meth}`meta_package_manager.manager.PackageManager.refiltered_search`
refine them.
```
```{code-block} shell-session
$ pacstall --search neovim
neovim @ pacstall-programs
neovim-git @ pacstall-programs
```
"""
output = self.run_cli("--search", query)
for match in self._SEARCH_REGEXP.finditer(output):
yield self.package(id=match.group("package_id"))
[docs]
@version_not_implemented
def install(self, package_id: str, version: str | None = None) -> str:
"""Install one package.
```{code-block} shell-session
$ pacstall --install neofetch
```
"""
return self.run_cli("--install", package_id)
[docs]
def upgrade_all_cli(self) -> tuple[str, ...]:
"""Generates the CLI to upgrade all packages.
```{code-block} shell-session
$ pacstall --upgrade
```
"""
return self.build_cli("--upgrade")
[docs]
@version_not_implemented
def upgrade_one_cli(
self,
package_id: str,
version: str | None = None,
) -> tuple[str, ...]:
"""Generates the CLI to upgrade one package by reinstalling it.
```{code-block} shell-session
$ pacstall --install neofetch
```
"""
return self.build_cli("--install", package_id)
[docs]
def remove(self, package_id: str) -> str:
"""Remove one package.
```{code-block} shell-session
$ pacstall --remove neofetch
```
"""
return self.run_cli("--remove", package_id)
[docs]
def sync(self) -> None:
"""Sync package metadata from remote repositories.
```{code-block} shell-session
$ pacstall --update
```
"""
self.run_cli("--update")