Source code for meta_package_manager.managers.pear

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

from ..manager import PackageManager

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

    from ..package import Package


RELEASE_STATES = ("snapshot", "devel", "alpha", "beta", "stable")
"""The quality states a PEAR release can carry.

Source: `PEAR_Validate::getValidStates()` in `PEAR/Validate.php`, repeated
verbatim in `PEAR/Common.php` and both REST clients.

The listing parsers key on this vocabulary because it is what separates a
package row from the table furniture around it: a row ends on one of these
lowercase words, while the `PACKAGE VERSION STATE` header ends on an uppercase
one and the `(no packages installed)` placeholder on a parenthesis.
"""

_STATES = "|".join(RELEASE_STATES)


[docs] class PEAR(PackageManager): """The PHP Extension and Application Repository. PEAR ships with PHP itself and installs libraries into the interpreter's `php_dir`, against its own registry at `pear.php.net`. ```{important} Whether that needs root is a property of the PHP install, not of PEAR: a distribution's `php_dir` is `/usr/share/php` and refuses an ordinary user with `Cannot install, php_dir for channel "pear.php.net" is not writable by the current user`. So the mutating operations carry privileged markers but leave them dormant, exactly as the other language managers do: `mpm --sudo`, or a `[mpm.overrides.pear] sudo = true` entry, escalates them. The better fix is to own the prefix instead. PEAR is fully relocatable, and every role directory has to move together: repointing `php_dir` alone fails late, on `failed to mkdir /usr/share/php/tests/...`, because `test_dir` is a separate setting. With `php_dir`, `bin_dir`, `data_dir`, `test_dir`, `doc_dir`, `cfg_dir`, `www_dir`, `man_dir`, `temp_dir`, `download_dir` and `cache_dir` all under one writable prefix, an install needs no privilege at all. ``` ```{caution} Every network operation costs one REST round-trip per package, and `pear.php.net` answers slowly: `outdated` measured 63 seconds warm and 105 seconds cold against a host carrying only six packages, so it approaches mpm's 120-second read-only cap on an inventory barely larger. Raise `mpm --timeout` on a host with more. `installed` is unaffected, reading the local registry with no network at all. ``` ```{note} `search` is not implemented. `pear search` resolves through the same per-package REST walk (`PEAR_REST_10::listAll()` with its `$basic` parameter false, one `p/<name>/info.xml` fetch apiece), and no run of it here ever returned: two attempts were abandoned after 15 and 30 minutes, the second having reached "50%". `remote-list` is no way around it either, costing 524 seconds on a cold cache. Both are far past the read-only cap, so mpm skips the operation and `install` falls through to installing the named package directly. ``` ```{note} A package is reported under its bare name, the spelling `pear install` takes. PEAR identities are really channel-qualified (`[channel/]package`), so two channels shipping the same name would collapse onto one entry; in practice `pear.php.net` is the only populated one. ``` Documentation: [PEAR manual](https://pear.php.net/manual/en/guide.users.commandline.cli.php). """ name = "PEAR" homepage_url = "https://pear.php.net" logo = "php" platforms = ALL_PLATFORMS requirement = ">=1.10.0" """The series every current distribution ships, and the one upstream still releases from (`1.10.18`, 2026-01-25). A conservative floor rather than a feature bisect: every verb driven here is older, the bare `upgrade` delegation to `doUpgradeAll` being present already in `1.9.5`. """ _INSTALLED_REGEXP = re.compile( rf"^(?P<package_id>\S+)\s+(?P<installed_version>\S+)\s+(?:{_STATES})\s*$", ) _OUTDATED_REGEXP = re.compile( rf"^\S+\s+(?P<package_id>\S+)" rf"\s+(?P<installed_version>\S+)\s+\((?:{_STATES})\)" rf"\s+(?P<latest_version>\S+)\s+\((?:{_STATES})\)" rf"\s+\S+\s*$", ) version_cli_options = ("version",) """`pear --version` is not a thing: it is rejected as `Command '--version' is not valid`, and the version comes from the `version` verb instead. """ version_regexes = (r"PEAR Version:\s+(?P<version>\S+)",) """ ```{code-block} shell-session $ pear version PEAR Version: 1.10.16 PHP Version: 8.4.24 Zend Engine Version: 4.4.24 Running on: Linux debian 6.12.94+deb13-arm64 #1 SMP Debian 6.12.94-1 (2026-06-20) aarch64 ``` """ @property def installed(self) -> Iterator[Package]: """Fetch installed packages. `--allchannels` is not optional: a bare `pear list` reports the default channel alone and silently omits everything installed from any other. The listing is a section per channel, and the parser keeps only the rows closing on a release state. ```{code-block} shell-session $ pear list --allchannels INSTALLED PACKAGES, CHANNEL __URI: ================================== (no packages installed) INSTALLED PACKAGES, CHANNEL DOC.PHP.NET: ======================================== (no packages installed) INSTALLED PACKAGES, CHANNEL PEAR.PHP.NET: ========================================= PACKAGE VERSION STATE Archive_Tar 1.5.0 stable Console_Getopt 1.4.3 stable PEAR 1.10.16 stable PEAR_Manpages 1.10.0 stable Structures_Graph 1.2.0 stable XML_Util 1.4.5 stable INSTALLED PACKAGES, CHANNEL PECL.PHP.NET: ========================================= (no packages installed) ``` """ output = self.run_cli("list", "--allchannels") yield from self.parse_regex_lines(self._INSTALLED_REGEXP, output) @property def outdated(self) -> Iterator[Package]: """Fetch outdated packages. `list-upgrades` walks every registered channel on its own, so it takes no `--allchannels`. Each version column carries its release state in parentheses, which is what tells a package row from the header above it. ```{code-block} shell-session $ pear list-upgrades PEAR.PHP.NET AVAILABLE UPGRADES (STABLE): ========================================= CHANNEL PACKAGE LOCAL REMOTE SIZE pear.php.net Archive_Tar 1.5.0 (stable) 1.6.0 (stable) 22kB pear.php.net PEAR 1.10.16 (stable) 1.10.18 (stable) 288kB ``` """ output = self.run_cli("list-upgrades") yield from self.parse_regex_lines(self._OUTDATED_REGEXP, output)
[docs] def install(self, package_id: str, version: str | None = None) -> str: """Install one package. A version is appended to the name with a dash, the `Package-1.1` form PEAR's own `install` help documents. PEAR package names use underscores, so the dash never collides with one. ```{code-block} shell-session $ pear install Text_Password downloading Text_Password-1.2.1.tgz ... Starting to download Text_Password-1.2.1.tgz (5,631 bytes) .....done: 5,631 bytes install ok: channel://pear.php.net/Text_Password-1.2.1 ``` ```{code-block} shell-session $ pear install Text_Password-1.2.1 Starting to download Text_Password-1.2.1.tgz (5,631 bytes) .....done: 5,631 bytes install ok: channel://pear.php.net/Text_Password-1.2.1 ``` """ # Marked privileged so --sudo / `[mpm.overrides.pear] sudo = true` can # escalate a system-PHP install; dormant by default. return self.run_cli( "install", f"{package_id}-{version}" if version else package_id, sudo=True, )
[docs] def upgrade_all_cli(self) -> tuple[str, ...]: """Generates the CLI to upgrade all outdated packages. `upgrade` with no package named is the documented spelling: the separate `upgrade-all` verb still exists but announces itself as deprecated in favour of this one, and both land on the same `doUpgradeAll` handler. ```{caution} PEAR is itself a PEAR package, so an upgrade of everything replaces the tool along with the libraries it manages. ``` ```{code-block} shell-session $ pear upgrade ``` """ return self.build_cli("upgrade", sudo=True)
[docs] def upgrade_one_cli( self, package_id: str, version: str | None = None, ) -> tuple[str, ...]: """Generates the CLI to upgrade the provided package. ```{code-block} shell-session $ pear upgrade Text_Password ``` """ return self.build_cli( "upgrade", f"{package_id}-{version}" if version else package_id, sudo=True, )
[docs] def remove(self, package_id: str) -> str: """Remove one package. ```{code-block} shell-session $ pear uninstall Text_Password uninstall ok: channel://pear.php.net/Text_Password-1.2.1 ``` """ return self.run_cli("uninstall", package_id, sudo=True)
[docs] def sync(self) -> None: """Sync package metadata. Refreshes the definition of every registered channel. A channel whose server answers badly is reported and stepped over, the run still exiting `0`. ```{code-block} shell-session $ pear update-channels Updating channel "doc.php.net" Error: Unable to create XML parser Invalid channel.xml file Updating channel "pear.php.net" Channel "pear.php.net" is up to date Updating channel "pecl.php.net" Update of Channel "pecl.php.net" succeeded ``` """ self.run_cli("update-channels", sudo=True)
[docs] def cleanup_cache(self) -> None: """Clear the cached REST responses. ```{code-block} shell-session $ pear clear-cache reading directory /tmp/pear/cache 100 cache entries cleared ``` """ self.run_cli("clear-cache", sudo=True)