# 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 json
import logging
import re
from typing import ClassVar
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.
```{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.
```
```{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.
```
"""
# `--classic` is forced on every invocation, and it is the whole basis of
# the scoping above. 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.
#
# 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.
#
# Search results are named without a triplet, since nothing is installed
# yet and a bare name resolves against the default triplet at install
# time.
operation_notes: ClassVar = {
"search": (
"Results are named without a triplet; a bare name resolves "
"against the default triplet at install time."
),
}
name = "vcpkg"
homepage_url = "https://vcpkg.io"
documentation_url = "https://learn.microsoft.com/en-us/vcpkg/"
repository_url = "https://github.com/microsoft/vcpkg"
wikipedia_url = "https://en.wikipedia.org/wiki/Vcpkg"
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.
"""
_COLUMN_GAP_REGEXP = re.compile(r"[ \t]{2,}")
"""Splits a row of the human search listing into its columns.
The widths are computed from the results rather than fixed, so the gap
between columns is the only stable separator. A single space never
separates them, descriptions being full of them.
"""
def _parse_column_listing(self, output: str) -> Iterator[Package]:
"""Parse the human search listing vcpkg prints when `--x-json` is ignored.
Rows are `port`, `version` and `description`, except that a port's
features repeat it as `port[feature]` carrying a description alone.
Those are skipped: mpm keys on the port, which has a row of its own,
and a feature has no version to report anyway.
The version is recognised by starting with a digit rather than by its
column, which is what separates a genuine version from the description
of a port that happens to declare none.
"""
for line in output.splitlines():
fields = self._COLUMN_GAP_REGEXP.split(line.strip())
package_id = fields[0]
if not package_id or "[" in package_id:
continue
version = None
if len(fields) > 1 and fields[1][:1].isdigit():
version = fields[1]
yield self.package(id=package_id, latest_version=version)
@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")
listing = self.parse_json(output)
if not isinstance(listing, dict):
return
for spec, data in listing.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.
```
```{caution}
`--x-json` is experimental, as its `x` prefix announces, and a vcpkg
predating it *ignores it silently* rather than refusing it: the command
succeeds and prints the human column listing instead. Decoding that as
JSON raises, and nothing isolates one manager's failure from the rest
of a fan-out, so an unhandled decode error would take down the entire
`mpm search` on any host carrying such a build. A version floor cannot
catch it, vcpkg numbering its releases by date, as
{attr}`~meta_package_manager.managers.vcpkg.VCPKG.requirement` explains,
so the column listing is parsed instead when the JSON does not decode.
The fallback matters beyond search: `install` picks its manager by
searching first, so a vcpkg whose search answers nothing can install
nothing either.
```
```{code-block} console
$ vcpkg --classic search excel --x-json
cpr 1.14.2 C++ Requests is a simple wrapper around libcurl
cpr[ssl] Enable SSL support
duckdb[excel] Statically link the excel extension into DuckDB
freexl 2.0.0#2 FreeXL is an open source library to extract valid data
```
"""
output = self.run_cli("search", query, "--x-json")
if not output.strip():
return
try:
results = json.loads(output)
except json.JSONDecodeError:
results = None
if isinstance(results, dict):
for port_name, data in results.items():
version = data.get("version") if isinstance(data, dict) else None
yield self.package(id=port_name, latest_version=version)
return
logging.debug(
"Search did not answer JSON: parsing the column listing instead.",
extra={"label": self.subject},
)
yield from self._parse_column_listing(output)
[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)