Source code for meta_package_manager.managers.vcpkg

# 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 json
import re

from extra_platforms import LINUX_LIKE, MACOS, WINDOWS

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 VCPKG(PackageManager): """C and C++ library manager, covering what it installs machine-wide. vcpkg has two modes and only one is a package manager in mpm's sense. In *manifest* mode it reads a `vcpkg.json` from a project tree and installs beside it, which is project scope and out of scope here, recorded among the project-scoped ecosystems of {doc}`/unsupported`. In *classic* mode it installs into its own root, shared by everything on the machine, which Microsoft's own documentation compares to `brew` or `apt`. That mode is what this wraps, on the same footing as the runtime managers mpm wraps for what they install globally. ```{important} `--classic` is forced on every invocation, and it is the whole basis of that scoping. vcpkg otherwise searches upwards from the working directory for a `vcpkg.json` and silently switches modes on finding one, so a listing taken inside a C++ project would report that project's dependencies instead of the machine's. Unlike the equivalent levers on other managers, this one is a documented, stable switch rather than a workaround. ``` ```{caution} A package is identified by its full specification, `name:triplet`, because that is vcpkg's own unit: the same library built for two triplets is two installations, removed independently. Search results are named without a triplet, since nothing is installed yet and a bare name resolves against the default triplet at install time. ``` ```{note} The inventory is read as JSON rather than from the human listing, which cannot be parsed safely: that listing pads the specification to a fixed fifty columns and truncates anything longer to exactly fifty characters, leaving no separator at all before the version. Real specifications exceed that width, so the rows whose identifier was already corrupted are also the rows a whitespace split would silently misread. ``` ```{warning} A vcpkg binary on `PATH` is not necessarily a working one. vcpkg is normally cloned and bootstrapped, and a packaged binary with no root configured errors on every operation asking for `VCPKG_ROOT` to be set. Homebrew ships exactly that, and says so in its own caveats. The failure is loud and self-explanatory rather than silent. ``` Documentation: [vcpkg classic mode](https://learn.microsoft.com/en-us/vcpkg/concepts/classic-mode). """ name = "vcpkg" homepage_url = "https://vcpkg.io" platforms = LINUX_LIKE, MACOS, WINDOWS requirement = None """No floor, because vcpkg's version is a release date rather than a semantic version and a requirement here is written in digits and comparisons alone, which a hyphenated date cannot be. Nothing is lost by it. Every interface used here shipped alongside manifest mode in 2020, so no release a user could plausibly be running lacks them, and a floor could not catch the case that would matter anyway: a locally built vcpkg reports a sentinel date far in the future precisely so that version checks in scripts always pass. """ pre_args = ("--classic",) """Pins every call to the machine-wide installation, whatever the working directory contains. """ version_regexes = (r"version (?P<version>\d{4}-\d{2}-\d{2})",) r"""Search the release date the version banner reports. ```{code-block} shell-session $ vcpkg --classic version vcpkg package management program version 2026-07-27-unknownhash See LICENSE.txt for license information. ``` The trailing component is a commit hash, or a packager's own marker, and is dropped. """ _OUTDATED_REGEXP = re.compile( r"^\t(?P<package_id>\S+)\s+(?P<installed_version>\S+) -> (?P<latest_version>\S+)$", ) """One upgradable package. Unlike the installed listing this one is written with a literal separator and no truncation, so it is parsed as text. """ @property def installed(self) -> Iterator[Package]: """Fetch installed packages. ```{code-block} shell-session $ vcpkg --classic list --x-json ``` ```{code-block} console { "zlib:x64-linux": { "package_name": "zlib", "triplet": "x64-linux", "version": "1.3.1", "port_version": 0, "features": [], "desc": ["A compression library"] } } ``` The port revision is appended to the version as vcpkg itself renders it, and only when it is not zero. """ output = self.run_cli("list", "--x-json") if not output.strip(): return for spec, data in json.loads(output).items(): if not isinstance(data, dict): continue version = data.get("version") port_version = data.get("port_version") or 0 if version and port_version: version = f"{version}#{port_version}" yield self.package(id=spec, installed_version=version) @property def outdated(self) -> Iterator[Package]: """Fetch outdated packages. vcpkg's `update` is a report rather than a mutation: in classic mode it compares what is installed against the ports catalog and prints the difference, changing nothing. It refuses to run in manifest mode at all, which the forced `--classic` keeps it out of. ```{code-block} shell-session $ vcpkg --classic update ``` ```{code-block} console Using local port versions. To update the local ports, use `git pull`. The following packages differ from their port versions: corrade:x64-windows 2020.06#4 -> 2020.06#5 openal-soft:x64-windows 1.22.2#5 -> 1.23.0 ``` """ output = self.run_cli("update") yield from ( self.package( id=match.group("package_id"), installed_version=match.group("installed_version"), latest_version=match.group("latest_version"), ) for match in map(self._OUTDATED_REGEXP.match, output.splitlines()) if match )
[docs] @search_capabilities(extended_support=False, exact_support=False) def search(self, query: str, extended: bool, exact: bool) -> Iterator[Package]: """Fetch matching packages. Read as JSON, whose keys are the port names. The human listing computes its column widths from the results and renders a port's features as rows of their own carrying no version, neither of which a single pattern reads reliably. ```{caution} Search does not support extended or exact matching. ``` ```{code-block} shell-session $ vcpkg --classic search zlib --x-json ``` """ output = self.run_cli("search", query, "--x-json") if not output.strip(): return for port_name, data in json.loads(output).items(): version = data.get("version") if isinstance(data, dict) else None yield self.package(id=port_name, latest_version=version)
[docs] @version_not_implemented def install(self, package_id: str, version: str | None = None) -> str: """Install one package. A bare name resolves against the default triplet; a full specification pins the one it names. ```{code-block} shell-session $ vcpkg --classic install zlib:x64-linux ``` """ return self.run_cli("install", package_id)
[docs] def upgrade_all_cli(self) -> tuple[str, ...]: """Generates the CLI to upgrade all packages. `--no-dry-run` is required for vcpkg to act at all: without it the command prints its plan, warns, and exits non-zero. ```{code-block} shell-session $ vcpkg --classic upgrade --no-dry-run ``` """ return self.build_cli("upgrade", "--no-dry-run")
[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. ```{code-block} shell-session $ vcpkg --classic upgrade --no-dry-run zlib:x64-linux ``` """ return self.build_cli("upgrade", "--no-dry-run", package_id)
[docs] def remove(self, package_id: str) -> str: """Removes a package. `--recurse` is deliberately not passed. vcpkg refuses to remove a package other installations depend on, listing them and exiting non-zero, and that refusal is the right outcome: the alternative would quietly remove packages the user never named. ```{code-block} shell-session $ vcpkg --classic remove zlib:x64-linux ``` """ return self.run_cli("remove", package_id)