Meta: Add a script to inspect installed vcpkg ports
This script inspects the actually-used versions of vcpkg ports in the Build/vcpkg directory. This is not just the ports listed in vcpkg.json, but the ports that were actually installed (and their dependencies). It prints the installed versions, chosen features, and more info for each port to either stdout as a table or to a file as JSON. It also contains a mode to diff two JSON exports. This makes it handy to know what changed when e.g. updating the vcpkg baseline.
This commit is contained in:
parent
8e2eee7654
commit
61a53553ab
1 changed files with 757 additions and 0 deletions
757
Meta/inspect-vcpkg-ports.py
Executable file
757
Meta/inspect-vcpkg-ports.py
Executable file
|
|
@ -0,0 +1,757 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2026-present, the Ladybird developers.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from typing import Any
|
||||
from typing import Optional
|
||||
|
||||
META_SOURCE_DIR = pathlib.Path(__file__).resolve().parent
|
||||
LADYBIRD_SOURCE_DIR = META_SOURCE_DIR.parent
|
||||
|
||||
|
||||
def relative_to_source_tree(path: pathlib.Path, source_root: pathlib.Path = LADYBIRD_SOURCE_DIR) -> str:
|
||||
try:
|
||||
return str(path.resolve().relative_to(source_root))
|
||||
except ValueError:
|
||||
return str(path)
|
||||
|
||||
|
||||
def read_json(path: pathlib.Path) -> Optional[Any]:
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as file:
|
||||
return json.load(file)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
def git_output(repository: pathlib.Path, *args: str) -> Optional[str]:
|
||||
if not repository.is_dir():
|
||||
return None
|
||||
|
||||
result = subprocess.run(
|
||||
["git", "-C", str(repository), *args],
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
|
||||
output = result.stdout.strip()
|
||||
return output if output else None
|
||||
|
||||
|
||||
def parse_control_paragraphs(path: pathlib.Path) -> list[dict[str, str]]:
|
||||
paragraphs: list[dict[str, str]] = []
|
||||
current_paragraph: dict[str, str] = {}
|
||||
current_field: Optional[str] = None
|
||||
|
||||
with open(path, "r", encoding="utf-8") as file:
|
||||
for raw_line in file:
|
||||
line = raw_line.rstrip("\n")
|
||||
|
||||
if not line:
|
||||
if current_paragraph:
|
||||
paragraphs.append(current_paragraph)
|
||||
current_paragraph = {}
|
||||
current_field = None
|
||||
continue
|
||||
|
||||
if line[0].isspace():
|
||||
if current_field is not None:
|
||||
current_paragraph[current_field] += "\n" + line.strip()
|
||||
continue
|
||||
|
||||
field, separator, value = line.partition(":")
|
||||
if not separator:
|
||||
continue
|
||||
|
||||
current_field = field
|
||||
current_paragraph[current_field] = value.strip()
|
||||
|
||||
if current_paragraph:
|
||||
paragraphs.append(current_paragraph)
|
||||
|
||||
return paragraphs
|
||||
|
||||
|
||||
def split_control_list(value: Optional[str]) -> list[str]:
|
||||
if not value:
|
||||
return []
|
||||
|
||||
entries: list[str] = []
|
||||
current_entry: list[str] = []
|
||||
nesting_depth = 0
|
||||
|
||||
for character in value:
|
||||
if character in "([":
|
||||
nesting_depth += 1
|
||||
elif character in ")]" and nesting_depth > 0:
|
||||
nesting_depth -= 1
|
||||
elif character == "," and nesting_depth == 0:
|
||||
entry = "".join(current_entry).strip()
|
||||
if entry:
|
||||
entries.append(entry)
|
||||
current_entry = []
|
||||
continue
|
||||
|
||||
current_entry.append(character)
|
||||
|
||||
entry = "".join(current_entry).strip()
|
||||
if entry:
|
||||
entries.append(entry)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def dependency_name(dependency: Any) -> Optional[str]:
|
||||
if isinstance(dependency, str):
|
||||
return dependency
|
||||
if isinstance(dependency, dict):
|
||||
name = dependency.get("name")
|
||||
if isinstance(name, str):
|
||||
return name
|
||||
return None
|
||||
|
||||
|
||||
def collect_manifest_dependency_names(manifest_path: pathlib.Path) -> tuple[set[str], set[str], Optional[str]]:
|
||||
manifest = read_json(manifest_path)
|
||||
if not isinstance(manifest, dict):
|
||||
return set(), set(), None
|
||||
|
||||
direct_dependencies = {
|
||||
name
|
||||
for name in (dependency_name(dependency) for dependency in manifest.get("dependencies", []))
|
||||
if name is not None
|
||||
}
|
||||
|
||||
feature_dependencies: set[str] = set()
|
||||
for feature in manifest.get("features", {}).values():
|
||||
if not isinstance(feature, dict):
|
||||
continue
|
||||
feature_dependencies.update(
|
||||
name
|
||||
for name in (dependency_name(dependency) for dependency in feature.get("dependencies", []))
|
||||
if name is not None
|
||||
)
|
||||
|
||||
builtin_baseline = manifest.get("builtin-baseline")
|
||||
if not isinstance(builtin_baseline, str):
|
||||
builtin_baseline = None
|
||||
|
||||
return direct_dependencies, feature_dependencies, builtin_baseline
|
||||
|
||||
|
||||
def exact_version(version: Optional[str], port_version: Optional[str]) -> Optional[str]:
|
||||
if not version:
|
||||
return None
|
||||
if port_version and port_version != "0":
|
||||
return f"{version}#{port_version}"
|
||||
return version
|
||||
|
||||
|
||||
def port_version_value(port_version: Optional[str]) -> int:
|
||||
if not port_version:
|
||||
return 0
|
||||
try:
|
||||
return int(port_version)
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
|
||||
def parse_installed_ports(installed_root: pathlib.Path) -> list[dict[str, Any]]:
|
||||
status_path = installed_root / "vcpkg" / "status"
|
||||
records: dict[tuple[str, str], dict[str, Any]] = {}
|
||||
|
||||
for paragraph in parse_control_paragraphs(status_path):
|
||||
if paragraph.get("Status") != "install ok installed":
|
||||
continue
|
||||
|
||||
name = paragraph.get("Package")
|
||||
triplet = paragraph.get("Architecture")
|
||||
if not name or not triplet:
|
||||
continue
|
||||
|
||||
key = (name, triplet)
|
||||
record = records.setdefault(
|
||||
key,
|
||||
{
|
||||
"name": name,
|
||||
"triplet": triplet,
|
||||
"version": None,
|
||||
"port_version": 0,
|
||||
"exact_version": None,
|
||||
"abi": None,
|
||||
"features": set(),
|
||||
"dependencies": set(),
|
||||
"base_dependencies": [],
|
||||
"feature_dependencies": {},
|
||||
"description": None,
|
||||
},
|
||||
)
|
||||
|
||||
dependencies = split_control_list(paragraph.get("Depends"))
|
||||
feature = paragraph.get("Feature")
|
||||
if feature:
|
||||
record["features"].add(feature)
|
||||
record["feature_dependencies"][feature] = dependencies
|
||||
record["dependencies"].update(dependencies)
|
||||
continue
|
||||
|
||||
version = paragraph.get("Version")
|
||||
port_version = paragraph.get("Port-Version")
|
||||
record["version"] = version
|
||||
record["port_version"] = port_version_value(port_version)
|
||||
record["exact_version"] = exact_version(version, port_version)
|
||||
record["abi"] = paragraph.get("Abi")
|
||||
record["base_dependencies"] = dependencies
|
||||
record["dependencies"].update(dependencies)
|
||||
record["description"] = paragraph.get("Description")
|
||||
|
||||
ports: list[dict[str, Any]] = []
|
||||
for record in records.values():
|
||||
record["features"] = sorted(record["features"])
|
||||
record["dependencies"] = sorted(record["dependencies"])
|
||||
record["feature_dependencies"] = {
|
||||
feature: dependencies for feature, dependencies in sorted(record["feature_dependencies"].items())
|
||||
}
|
||||
ports.append(record)
|
||||
|
||||
return sorted(ports, key=lambda port: (port["triplet"], port["name"]))
|
||||
|
||||
|
||||
def parse_abi_info_file(path: pathlib.Path) -> Optional[dict[str, str]]:
|
||||
if not path.is_file():
|
||||
return None
|
||||
|
||||
entries: dict[str, str] = {}
|
||||
with open(path, "r", encoding="utf-8") as file:
|
||||
for raw_line in file:
|
||||
line = raw_line.strip()
|
||||
if not line:
|
||||
continue
|
||||
key, separator, value = line.partition(" ")
|
||||
if separator:
|
||||
entries[key] = value
|
||||
else:
|
||||
entries[key] = ""
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def abi_info_for_port(
|
||||
source_root: pathlib.Path, vcpkg_root: pathlib.Path, name: str, triplet: str
|
||||
) -> Optional[dict[str, Any]]:
|
||||
path = vcpkg_root / "buildtrees" / name / f"{triplet}.vcpkg_abi_info.txt"
|
||||
entries = parse_abi_info_file(path)
|
||||
if entries is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
"path": relative_to_source_tree(path, source_root),
|
||||
"entries": entries,
|
||||
"features": entries.get("features", "").split(),
|
||||
"portfile_sha256": entries.get("portfile.cmake"),
|
||||
"port_manifest_sha256": entries.get("vcpkg.json"),
|
||||
"ports_cmake_sha256": entries.get("ports.cmake"),
|
||||
"triplet_abi": entries.get("triplet_abi"),
|
||||
}
|
||||
|
||||
|
||||
def checksum_values(package: dict[str, Any]) -> list[dict[str, str]]:
|
||||
checksums = package.get("checksums", [])
|
||||
if not isinstance(checksums, list):
|
||||
return []
|
||||
|
||||
result: list[dict[str, str]] = []
|
||||
for checksum in checksums:
|
||||
if not isinstance(checksum, dict):
|
||||
continue
|
||||
algorithm = checksum.get("algorithm")
|
||||
value = checksum.get("checksumValue")
|
||||
if isinstance(algorithm, str) and isinstance(value, str):
|
||||
result.append({"algorithm": algorithm, "value": value})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def spdx_info_for_port(
|
||||
source_root: pathlib.Path, installed_root: pathlib.Path, name: str, triplet: str
|
||||
) -> Optional[dict[str, Any]]:
|
||||
path = installed_root / triplet / "share" / name / "vcpkg.spdx.json"
|
||||
spdx = read_json(path)
|
||||
if not isinstance(spdx, dict):
|
||||
return None
|
||||
|
||||
packages = spdx.get("packages", [])
|
||||
if not isinstance(packages, list):
|
||||
packages = []
|
||||
|
||||
package_by_id = {
|
||||
package.get("SPDXID"): package for package in packages if isinstance(package, dict) and package.get("SPDXID")
|
||||
}
|
||||
|
||||
def package_details(spdx_id: str) -> Optional[dict[str, Any]]:
|
||||
package = package_by_id.get(spdx_id)
|
||||
if not isinstance(package, dict):
|
||||
return None
|
||||
|
||||
return {
|
||||
"name": package.get("name"),
|
||||
"version": package.get("versionInfo"),
|
||||
"download_location": package.get("downloadLocation"),
|
||||
"homepage": package.get("homepage"),
|
||||
"license": package.get("licenseConcluded"),
|
||||
"checksums": checksum_values(package),
|
||||
}
|
||||
|
||||
resources: list[dict[str, Any]] = []
|
||||
for package in packages:
|
||||
if not isinstance(package, dict):
|
||||
continue
|
||||
spdx_id = package.get("SPDXID")
|
||||
if not isinstance(spdx_id, str) or not spdx_id.startswith("SPDXRef-resource-"):
|
||||
continue
|
||||
resources.append(
|
||||
{
|
||||
"name": package.get("name"),
|
||||
"download_location": package.get("downloadLocation"),
|
||||
"checksums": checksum_values(package),
|
||||
}
|
||||
)
|
||||
|
||||
creation_info = spdx.get("creationInfo", {})
|
||||
if not isinstance(creation_info, dict):
|
||||
creation_info = {}
|
||||
|
||||
creators = creation_info.get("creators", [])
|
||||
if not isinstance(creators, list):
|
||||
creators = []
|
||||
|
||||
return {
|
||||
"path": relative_to_source_tree(path, source_root),
|
||||
"name": spdx.get("name"),
|
||||
"created": creation_info.get("created"),
|
||||
"creators": creators,
|
||||
"port": package_details("SPDXRef-port"),
|
||||
"binary": package_details("SPDXRef-binary"),
|
||||
"resources": resources,
|
||||
}
|
||||
|
||||
|
||||
def configured_overlay_ports(source_root: pathlib.Path) -> list[pathlib.Path]:
|
||||
configuration = read_json(source_root / "vcpkg-configuration.json")
|
||||
if not isinstance(configuration, dict):
|
||||
return []
|
||||
|
||||
overlay_ports = configuration.get("overlay-ports", [])
|
||||
if not isinstance(overlay_ports, list):
|
||||
return []
|
||||
|
||||
paths: list[pathlib.Path] = []
|
||||
for overlay_port in overlay_ports:
|
||||
if not isinstance(overlay_port, str):
|
||||
continue
|
||||
path = pathlib.Path(overlay_port)
|
||||
if not path.is_absolute():
|
||||
path = source_root / path
|
||||
paths.append(path)
|
||||
|
||||
return paths
|
||||
|
||||
|
||||
def port_source_info(source_root: pathlib.Path, vcpkg_root: pathlib.Path, name: str) -> dict[str, Optional[str]]:
|
||||
for overlay_path in configured_overlay_ports(source_root):
|
||||
if overlay_path.name == name and (overlay_path / "vcpkg.json").is_file():
|
||||
return {
|
||||
"kind": "overlay",
|
||||
"directory": relative_to_source_tree(overlay_path, source_root),
|
||||
}
|
||||
|
||||
nested_overlay_path = overlay_path / name
|
||||
if (nested_overlay_path / "vcpkg.json").is_file():
|
||||
return {
|
||||
"kind": "overlay",
|
||||
"directory": relative_to_source_tree(nested_overlay_path, source_root),
|
||||
}
|
||||
|
||||
builtin_path = vcpkg_root / "ports" / name
|
||||
if (builtin_path / "vcpkg.json").is_file():
|
||||
return {
|
||||
"kind": "builtin",
|
||||
"directory": relative_to_source_tree(builtin_path, source_root),
|
||||
}
|
||||
|
||||
return {
|
||||
"kind": "unknown",
|
||||
"directory": None,
|
||||
}
|
||||
|
||||
|
||||
def manifest_path_for_installed_root(source_root: pathlib.Path, installed_root: pathlib.Path) -> pathlib.Path:
|
||||
manifest_info = read_json(installed_root / "vcpkg" / "manifest-info.json")
|
||||
if isinstance(manifest_info, dict):
|
||||
manifest_path = manifest_info.get("manifest-path")
|
||||
if isinstance(manifest_path, str):
|
||||
return pathlib.Path(manifest_path)
|
||||
|
||||
return source_root / "vcpkg.json"
|
||||
|
||||
|
||||
def discover_installed_roots(
|
||||
source_root: pathlib.Path, build_dir: pathlib.Path, vcpkg_root: pathlib.Path
|
||||
) -> list[pathlib.Path]:
|
||||
candidates: list[pathlib.Path] = []
|
||||
|
||||
env_installed_root = os.environ.get("VCPKG_INSTALLED_DIR")
|
||||
if env_installed_root:
|
||||
candidates.append(pathlib.Path(env_installed_root))
|
||||
|
||||
for status_path in build_dir.glob("*/vcpkg_installed/vcpkg/status"):
|
||||
candidates.append(status_path.parent.parent)
|
||||
|
||||
manifest_installed_root = source_root / "vcpkg_installed"
|
||||
if (manifest_installed_root / "vcpkg" / "status").is_file():
|
||||
candidates.append(manifest_installed_root)
|
||||
|
||||
classic_installed_root = vcpkg_root / "installed"
|
||||
if (classic_installed_root / "vcpkg" / "status").is_file():
|
||||
candidates.append(classic_installed_root)
|
||||
|
||||
unique_candidates: list[pathlib.Path] = []
|
||||
seen: set[pathlib.Path] = set()
|
||||
for candidate in candidates:
|
||||
resolved_candidate = candidate.resolve()
|
||||
if resolved_candidate in seen:
|
||||
continue
|
||||
seen.add(resolved_candidate)
|
||||
if (resolved_candidate / "vcpkg" / "status").is_file():
|
||||
unique_candidates.append(resolved_candidate)
|
||||
|
||||
return sorted(unique_candidates)
|
||||
|
||||
|
||||
def build_report() -> dict[str, Any]:
|
||||
source_root = LADYBIRD_SOURCE_DIR.resolve()
|
||||
vcpkg_root = pathlib.Path(os.environ.get("VCPKG_ROOT") or source_root / "Build" / "vcpkg").resolve()
|
||||
installed_roots = discover_installed_roots(source_root, source_root / "Build", vcpkg_root)
|
||||
|
||||
if not installed_roots:
|
||||
print(
|
||||
"No vcpkg install trees found. Set VCPKG_INSTALLED_DIR or run a manifest install first.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
vcpkg_revision = git_output(vcpkg_root, "rev-parse", "HEAD")
|
||||
vcpkg_dirty_status = git_output(vcpkg_root, "status", "--porcelain")
|
||||
|
||||
report = {
|
||||
"source_root": str(source_root),
|
||||
"vcpkg_root": str(vcpkg_root),
|
||||
"vcpkg_root_revision": vcpkg_revision,
|
||||
"vcpkg_root_dirty": bool(vcpkg_dirty_status),
|
||||
"installed_roots": [],
|
||||
}
|
||||
|
||||
for installed_root in installed_roots:
|
||||
manifest_path = manifest_path_for_installed_root(source_root, installed_root)
|
||||
direct_dependencies, feature_dependencies, builtin_baseline = collect_manifest_dependency_names(manifest_path)
|
||||
|
||||
ports = []
|
||||
for port in parse_installed_ports(installed_root):
|
||||
name = port["name"]
|
||||
triplet = port["triplet"]
|
||||
port.update(
|
||||
{
|
||||
"declared_in_manifest_dependencies": name in direct_dependencies,
|
||||
"declared_in_manifest_feature_dependencies": name in feature_dependencies,
|
||||
"port_source": port_source_info(source_root, vcpkg_root, name),
|
||||
"abi_info": abi_info_for_port(source_root, vcpkg_root, name, triplet),
|
||||
"spdx": spdx_info_for_port(source_root, installed_root, name, triplet),
|
||||
}
|
||||
)
|
||||
ports.append(port)
|
||||
|
||||
report["installed_roots"].append(
|
||||
{
|
||||
"path": str(installed_root),
|
||||
"manifest_path": str(manifest_path),
|
||||
"manifest_builtin_baseline": builtin_baseline,
|
||||
"ports": ports,
|
||||
"port_count": len(ports),
|
||||
}
|
||||
)
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def report_has_duplicate_port_keys(report: dict[str, Any]) -> bool:
|
||||
keys: set[str] = set()
|
||||
|
||||
for installed_root in report.get("installed_roots", []):
|
||||
for port in installed_root.get("ports", []):
|
||||
name = port.get("name")
|
||||
triplet = port.get("triplet")
|
||||
if not name or not triplet:
|
||||
continue
|
||||
|
||||
key = f"{triplet}/{name}"
|
||||
if key in keys:
|
||||
return True
|
||||
keys.add(key)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def report_ports_by_key(report: dict[str, Any], root_qualified: bool) -> dict[str, dict[str, Any]]:
|
||||
entries_by_key: dict[str, list[dict[str, Any]]] = {}
|
||||
|
||||
for installed_root in report.get("installed_roots", []):
|
||||
installed_root_path = installed_root.get("path", "")
|
||||
for port in installed_root.get("ports", []):
|
||||
name = port.get("name")
|
||||
triplet = port.get("triplet")
|
||||
if not name or not triplet:
|
||||
continue
|
||||
|
||||
key = f"{triplet}/{name}"
|
||||
entries_by_key.setdefault(key, []).append(
|
||||
{
|
||||
"installed_root": installed_root_path,
|
||||
"port": port,
|
||||
}
|
||||
)
|
||||
|
||||
if not root_qualified:
|
||||
return {key: entries[0] for key, entries in entries_by_key.items()}
|
||||
|
||||
root_qualified_entries: dict[str, dict[str, Any]] = {}
|
||||
for key, entries in entries_by_key.items():
|
||||
for entry in entries:
|
||||
root_qualified_key = f"{entry['installed_root']}:{key}"
|
||||
root_qualified_entries[root_qualified_key] = entry
|
||||
|
||||
return root_qualified_entries
|
||||
|
||||
|
||||
def load_json_report(path: str) -> dict[str, Any]:
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as file:
|
||||
report = json.load(file)
|
||||
except FileNotFoundError:
|
||||
print(f"{path}: file not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except json.JSONDecodeError as error:
|
||||
print(f"{path}: invalid JSON: {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not isinstance(report, dict) or not isinstance(report.get("installed_roots"), list):
|
||||
print(f"{path}: not a JSON report generated by this script", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def port_version(entry: Optional[dict[str, Any]]) -> Optional[str]:
|
||||
if entry is None:
|
||||
return None
|
||||
|
||||
version = entry["port"].get("exact_version")
|
||||
return version if isinstance(version, str) else None
|
||||
|
||||
|
||||
def port_identity(key: str, entry: Optional[dict[str, Any]]) -> dict[str, Optional[str]]:
|
||||
if entry is None:
|
||||
return {
|
||||
"key": key,
|
||||
"name": key.rsplit("/", 1)[-1],
|
||||
"triplet": None,
|
||||
}
|
||||
|
||||
port = entry["port"]
|
||||
return {
|
||||
"key": key,
|
||||
"name": port.get("name"),
|
||||
"triplet": port.get("triplet"),
|
||||
}
|
||||
|
||||
|
||||
def build_diff(old_path: str, new_path: str) -> dict[str, Any]:
|
||||
old_report = load_json_report(old_path)
|
||||
new_report = load_json_report(new_path)
|
||||
root_qualified = report_has_duplicate_port_keys(old_report) or report_has_duplicate_port_keys(new_report)
|
||||
old_ports = report_ports_by_key(old_report, root_qualified)
|
||||
new_ports = report_ports_by_key(new_report, root_qualified)
|
||||
|
||||
version_changes = []
|
||||
for key in sorted(set(old_ports) | set(new_ports)):
|
||||
old_entry = old_ports.get(key)
|
||||
new_entry = new_ports.get(key)
|
||||
old_version = port_version(old_entry)
|
||||
new_version = port_version(new_entry)
|
||||
if old_version == new_version:
|
||||
continue
|
||||
|
||||
identity = port_identity(key, new_entry or old_entry)
|
||||
version_changes.append(
|
||||
{
|
||||
**identity,
|
||||
"old_version": old_version,
|
||||
"new_version": new_version,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"old_report": old_path,
|
||||
"new_report": new_path,
|
||||
"summary": {
|
||||
"version_changes": len(version_changes),
|
||||
},
|
||||
"version_changes": version_changes,
|
||||
}
|
||||
|
||||
|
||||
def format_version(value: Any) -> str:
|
||||
return "-" if value is None else str(value)
|
||||
|
||||
|
||||
def diff_text(diff: dict[str, Any]) -> str:
|
||||
rows = [
|
||||
[change["name"], format_version(change["old_version"]), format_version(change["new_version"])]
|
||||
for change in diff["version_changes"]
|
||||
]
|
||||
if not rows:
|
||||
return "No version changes."
|
||||
|
||||
lines = []
|
||||
headers = ["Port", "Old version", "New version"]
|
||||
widths = [len(header) for header in headers]
|
||||
for row in rows:
|
||||
for index, value in enumerate(row):
|
||||
widths[index] = max(widths[index], len(value))
|
||||
|
||||
lines.append(" ".join(header.ljust(widths[index]) for index, header in enumerate(headers)))
|
||||
lines.append(" ".join("-" * width for width in widths))
|
||||
for row in rows:
|
||||
lines.append(" ".join(value.ljust(widths[index]) for index, value in enumerate(row)))
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def table_text(report: dict[str, Any]) -> str:
|
||||
lines: list[str] = []
|
||||
source_root = pathlib.Path(report["source_root"])
|
||||
vcpkg_revision = report.get("vcpkg_root_revision") or "unknown"
|
||||
dirty_suffix = " dirty" if report.get("vcpkg_root_dirty") else ""
|
||||
lines.append(
|
||||
f"VCPKG_ROOT: {relative_to_source_tree(pathlib.Path(report['vcpkg_root']), source_root)} "
|
||||
f"({vcpkg_revision}{dirty_suffix})"
|
||||
)
|
||||
|
||||
installed_roots = report.get("installed_roots", [])
|
||||
for installed_root in installed_roots:
|
||||
lines.append("")
|
||||
lines.append(f"Installed root: {relative_to_source_tree(pathlib.Path(installed_root['path']), source_root)}")
|
||||
lines.append(f"Manifest: {relative_to_source_tree(pathlib.Path(installed_root['manifest_path']), source_root)}")
|
||||
if installed_root.get("manifest_builtin_baseline"):
|
||||
lines.append(f"Manifest baseline: {installed_root['manifest_builtin_baseline']}")
|
||||
lines.append(f"Installed ports: {installed_root['port_count']}")
|
||||
lines.append("")
|
||||
|
||||
rows = []
|
||||
for port in installed_root["ports"]:
|
||||
spdx = port.get("spdx") or {}
|
||||
resources = spdx.get("resources") or []
|
||||
abi = port.get("abi") or ""
|
||||
abi_info = port.get("abi_info") or {}
|
||||
rows.append(
|
||||
[
|
||||
port["name"],
|
||||
port["triplet"],
|
||||
port.get("exact_version") or "-",
|
||||
",".join(port.get("features") or []) or "-",
|
||||
abi[:12] if abi else "-",
|
||||
(port.get("port_source") or {}).get("kind") or "-",
|
||||
str(len(resources)),
|
||||
"yes" if abi_info else "no",
|
||||
]
|
||||
)
|
||||
|
||||
headers = ["Port", "Triplet", "Version", "Features", "ABI", "Source", "Resources", "ABI info"]
|
||||
widths = [len(header) for header in headers]
|
||||
for row in rows:
|
||||
for index, value in enumerate(row):
|
||||
widths[index] = max(widths[index], len(value))
|
||||
|
||||
lines.append(" ".join(header.ljust(widths[index]) for index, header in enumerate(headers)))
|
||||
lines.append(" ".join("-" * width for width in widths))
|
||||
for row in rows:
|
||||
lines.append(" ".join(value.ljust(widths[index]) for index, value in enumerate(row)))
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def write_json_output(path: str, payload: dict[str, Any]) -> None:
|
||||
output_path = pathlib.Path(path)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def write_output(args: argparse.Namespace, report: dict[str, Any]) -> None:
|
||||
if args.output:
|
||||
write_json_output(args.output, report)
|
||||
return
|
||||
|
||||
print(table_text(report))
|
||||
|
||||
|
||||
def write_diff_output(args: argparse.Namespace, diff: dict[str, Any]) -> None:
|
||||
if args.output:
|
||||
write_json_output(args.output, diff)
|
||||
return
|
||||
|
||||
print(diff_text(diff))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Report exact versions of installed vcpkg ports from manifest-mode install trees, "
|
||||
"including transitive ports."
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"--diff",
|
||||
nargs=2,
|
||||
metavar=("OLD_JSON", "NEW_JSON"),
|
||||
help="Compare port versions in two JSON reports previously exported by this script.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
help="Write JSON output to this path instead of printing a table to stdout.",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.diff:
|
||||
diff = build_diff(args.diff[0], args.diff[1])
|
||||
write_diff_output(args, diff)
|
||||
return
|
||||
|
||||
report = build_report()
|
||||
write_output(args, report)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in a new issue