diff options
| author | Leitwolf11 | 2026-10-25 13:57:03 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-10-25 13:57:03 +0300 |
| commit | 45f8c539e86099cb80ecf29b0bf2b3340e141001 (patch) | |
| tree | cf7f28b76fb2994a3750ecdd127b5738f4d56ad1 /tools/scripts/updateThirdParty.py | |
| parent | d2e3e074bb2e1b8ffd9fc990d0b87667ea602f58 (diff) | |
| download | catch3-45f8c539e86099cb80ecf29b0bf2b3340e141001.tar.gz | |
Read and write JSON with yyjson and JUnit XML with pugixml
The runner's hand-written JSON parser and writer, and its XML escaping,
are replaced by two established MIT libraries, vendored in third_party/:
yyjson 0.13.0 and pugixml 1.16. JsonValue and the JUnit model stay as they
are.
Both are compiled into Catch3's own translation units, so a project that
links its own yyjson or pugixml is not affected:
- src/Progmasoft/Runner/Yyjson.inl includes yyjson.c into Json.cpp with
yyjson_api defined as static, so no yyjson symbol leaves that file.
- src/Progmasoft/PugiXml.inl includes pugixml header-only into
XmlWriter.cpp with its namespace renamed to Progmasoft_Catch3_Pugi.
- Both are read as system headers, outside the warnings Catch3 builds
with; they are .inl files, which the header install rules leave out.
JSON keeps its behaviour: strings are bytes and come back as written
(invalid UTF-8 is allowed both ways), non-finite numbers are written as
null, documents nested deeper than 256 levels are refused while they are
converted, and whole numbers are written as integers. Fractions are now
written as the shortest text that reads back as the same double.
JUnit XML stays equivalent; pugixml writes ' and > in attributes as they
are, U+FFFD as the character itself and empty elements as <x />. The
tests follow.
third_party/{yyjson,pugixml} each have BUILD.bazel, CMakeLists.txt,
meson.build, xmake.lua and premake5.lua, which only provide the include
directory to the library; CMake links them through BUILD_INTERFACE.
tools/scripts/updateThirdParty.py pins the version and sha256 of each
archive; --check, now a step of the header and license workflow, fails
when a vendored file differs from its archive, and --update writes them.
Renovate proposes new versions through a regex manager on that table.
The amalgamation now has a third file, catch_amalgamated_third_party.hpp,
with both wrappers and the libraries inlined; catch_amalgamated.cpp
includes it once. The documents and the release notes say so.
Diffstat (limited to 'tools/scripts/updateThirdParty.py')
| -rw-r--r-- | tools/scripts/updateThirdParty.py | 103 |
1 files changed, 103 insertions, 0 deletions
diff --git a/tools/scripts/updateThirdParty.py b/tools/scripts/updateThirdParty.py new file mode 100644 index 00000000..a28ead3c --- /dev/null +++ b/tools/scripts/updateThirdParty.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Progmasoft. +# SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 + +"""Keeps third_party/ equal to the pinned release archives of its libraries. + + updateThirdParty.py --check fails when a vendored file differs + updateThirdParty.py --update writes the files of the pinned archives + +The table below is the only record of the versions. Renovate proposes a new +version by changing "version"; whoever takes it sets "sha256" to the digest +of the new archive, after reading the release, and runs --update. +""" + +import argparse +import hashlib +import io +import sys +import tarfile +import urllib.request +from pathlib import Path + +from scriptCommon import catchPath + +LIBRARIES = { + "yyjson": { + # renovate: datasource=github-tags depName=ibireme/yyjson + "version": "0.13.0", + "url": "https://github.com/ibireme/yyjson/archive/refs/tags/{version}.tar.gz", + "sha256": "34e0f62a2bc11ab20d601e8ca1cc2b2079503aa45119a19133d89d19b94a0fae", + "files": {"src/yyjson.c": "yyjson.c", "src/yyjson.h": "yyjson.h"}, + }, + "pugixml": { + # renovate: datasource=github-tags depName=zeux/pugixml extractVersion=^v(?<version>.+)$ + "version": "1.16", + "url": "https://github.com/zeux/pugixml/archive/refs/tags/v{version}.tar.gz", + "sha256": "357bcab8877dc9943f355d3a72daba1b053238ba955f50fa81586afb65090219", + "files": { + "src/pugiconfig.hpp": "pugiconfig.hpp", + "src/pugixml.cpp": "pugixml.cpp", + "src/pugixml.hpp": "pugixml.hpp", + }, + }, +} + + +def archive_files(name: str, library: dict) -> dict: + """The files of the pinned archive, after its digest is checked.""" + url = library["url"].format(version=library["version"]) + with urllib.request.urlopen(url, timeout=60) as response: + data = response.read() + digest = hashlib.sha256(data).hexdigest() + if digest != library["sha256"]: + sys.exit(f"{name}: {url} has sha256 {digest}, " + f"the table pins {library['sha256']}") + files = {} + with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as archive: + for member in archive.getmembers(): + # The archive has one top directory, named after the release. + inner = member.name.split("/", 1)[-1] + if member.isfile() and inner in library["files"]: + extracted = archive.extractfile(member) + assert extracted is not None + files[library["files"][inner]] = extracted.read() + missing = set(library["files"].values()) - set(files) + if missing: + sys.exit(f"{name}: the archive has no {', '.join(sorted(missing))}") + return files + + +def normalized(data: bytes) -> bytes: + # Git may check the files out with CRLF on Windows. + return data.replace(b"\r\n", b"\n") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + mode = parser.add_mutually_exclusive_group(required=True) + mode.add_argument("--check", action="store_true", + help="fail when a vendored file differs from its archive") + mode.add_argument("--update", action="store_true", + help="write the files of the pinned archives") + arguments = parser.parse_args() + + differences = 0 + for name, library in LIBRARIES.items(): + directory = Path(catchPath) / "third_party" / name + for file_name, data in archive_files(name, library).items(): + target = directory / file_name + if arguments.update: + target.write_bytes(data) + print(f"{name} {library['version']}: wrote {target.name}") + elif not target.is_file() or normalized(target.read_bytes()) != normalized(data): + print(f"{name} {library['version']}: {target} differs from the archive") + differences += 1 + if differences: + print("Run tools/scripts/updateThirdParty.py --update.") + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) |