Source code for meta_package_manager.managers.miktex

# 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 extra_platforms import LINUX, WINDOWS

from ..capabilities import version_not_implemented
from ..manager import PackageManager

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

    from ..package import Package


_LIST_TEMPLATE = "{isInstalled} {id} {version}"
"""Row format asked of MiKTeX's own listing.

MiKTeX has no listing restricted to installed packages: `list` walks its whole
catalog, several thousand entries. Prepending the installed flag is what lets
that dump be filtered back down to an inventory, and the version is asked for
in the same breath.

```{note}
This template is also why MiKTeX needs a class rather than a declarative
definition. A definition's arguments are scanned for mpm's own
``{placeholder}`` tokens and refuse any it does not recognize, and MiKTeX's
field names are lowercase, so `{id}` and `{version}` are read as mpm
placeholders and rejected. The uppercase query formats of other managers, like
rpm's ``%{NAME}``, slip past that check; these cannot.
```
"""


[docs] class MiKTeX(PackageManager): """TeX package manager of the MiKTeX distribution. Reaches MiKTeX's own package repositories, which are distinct from the catalog [`tlmgr`](tlmgr.md) reaches for TeX Live: two distributions, two managers, rather than one wrapped twice. ```{note} Linux and Windows only, and that follows the tool rather than a choice. The `packages` command arrived in MiKTeX `22.3`, while the newest macOS build MiKTeX publishes is `22.1`, which predates it. The only package surface on macOS is the standalone binary that happens to share this project's name, which is reason enough never to reach for it. ``` ```{caution} `--admin` is deliberately never passed, and the reasoning inverts what its name suggests. Left alone, MiKTeX reports the packages installed for the user *and* those installed system-wide; the flag narrows that to the system-wide ones alone. The default is therefore already the wider listing, and passing it would drop rows rather than pin them. It is fatal outright on an installation that is not shared, and warns about privileges it may lack when run without them. The cost is that removing a system-wide package is out of reach, needing both that flag and administrator rights. ``` ```{important} Queries pass `--disable-installer`. MiKTeX installs packages on the fly by default, so without it reading the inventory is not guaranteed to leave the system as it found it. ``` ```{note} No `outdated`, though MiKTeX does have a non-mutating check: it prints bare package names and no version of any kind, where reporting something as outdated needs a version to report it against. The only route to one is asking after each package individually, thousands of invocations for a single answer. `upgrade --all` is unaffected. No `search`, MiKTeX having no such command, and no `cleanup`: nothing in the package surface purges a cache or sweeps orphans. ``` ```{warning} The command that upgrades everything is `update`. MiKTeX's `upgrade` is a different thing entirely, taking a package *level* such as `basic` or `complete` and erroring without one, so it is never used here. ``` Documentation: [MiKTeX packages](https://docs.miktex.org/manual/miktex-packages.html). """ name = "MiKTeX" homepage_url = "https://miktex.org" logo = "latex" platforms = LINUX, WINDOWS requirement = ">=22.3" """The release that introduced the `packages` command this drives.""" version_regexes = (r"\(MiKTeX (?P<version>\d+(?:\.\d+)*)",) r"""Search the release named in parentheses. ```{code-block} console $ miktex --version One MiKTeX Utility 1.12.0 (MiKTeX 26.5) ``` Two numbers share that line: the utility's own version first, then the MiKTeX release, which is the one that means anything here. The parenthesis is what tells them apart. A word such as the build's word size may follow the release, which the pattern stops before. """ _INSTALLED_REGEXP = re.compile( r"^(?:true|1) (?P<package_id>\S+) ?(?P<installed_version>.*)$", ) """One installed package of the catalog listing. Rows for packages that are not installed carry the opposite flag and are skipped. The flag is matched permissively because MiKTeX renders it through a formatting library whose boolean output could not be observed, only derived. The version is free-form and often empty, in which case the package is reported without one rather than with a placeholder. """ @property def installed(self) -> Iterator[Package]: """Fetch installed packages. ```{code-block} console $ miktex --disable-installer packages list --template "{isInstalled} {id} {version}" true amsmath 2.17n false zwpagelayout true fancyhdr 4.0.3 ``` """ output = self.run_cli( "--disable-installer", "packages", "list", "--template", _LIST_TEMPLATE, ) yield from self.parse_regex_lines(self._INSTALLED_REGEXP, output)
[docs] @version_not_implemented def install(self, package_id: str, version: str | None = None) -> str: """Install one package. ```{code-block} shell-session $ miktex packages install fancyhdr ``` ```{caution} MiKTeX fails outright when the package is already installed, rather than treating it as a no-op. ``` """ return self.run_cli("packages", "install", package_id)
[docs] def upgrade_all_cli(self) -> tuple[str, ...]: """Generates the CLI to upgrade all packages. ```{code-block} shell-session $ miktex packages update ``` """ return self.build_cli("packages", "update")
[docs] @version_not_implemented def upgrade_one_cli( self, package_id: str, version: str | None = None, ) -> tuple[str, ...]: """Generates the CLI to upgrade the package provided as parameter. Naming packages genuinely restricts the run to them, the rest being reported as already up to date. ```{code-block} shell-session $ miktex packages update fancyhdr ``` """ return self.build_cli("packages", "update", package_id)
[docs] def remove(self, package_id: str) -> str: """Removes a package. ```{code-block} shell-session $ miktex packages remove fancyhdr ``` ```{caution} MiKTeX fails outright when the package is not installed. ``` """ return self.run_cli("packages", "remove", package_id)
[docs] def sync(self) -> None: """Sync package metadata. ```{code-block} shell-session $ miktex packages update-package-database ``` """ self.run_cli("packages", "update-package-database")