Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/tools/RepositoryReport.cpp
diff options
context:
space:
mode:
authorLeitwolf112026-11-01 22:16:14 +0300
committerLeitwolf112026-11-01 22:16:14 +0300
commitda3c9d0880dbd4a74e0e43f092d10f5ae42c5043 (patch)
tree4676a526585dbb0e2f6d8c9c18978ed01092c2e2 /tools/RepositoryReport.cpp
parent241b5567d09d59e82bff18287c32ada480deb8c0 (diff)
downloadcatch3-da3c9d0880dbd4a74e0e43f092d10f5ae42c5043.tar.gz
Move the tools from Python to C++17 and Bazel
The scripts in use become programs run with `bazelisk run //tools:<tool>`, and arguments follow `--`: amalgamate the single-file distribution; --check only compares it check licenses, convenience headers, header guards, file names, versions and whether the distribution is up to date third-party compares third_party/ with its archives, or writes it version prints the version, or moves every place that states it release-notes prints a section of the release notes repo-info reports the checkout and the tools on PATH; --json writes it through yyjson, with the fields of Tala's repo-info amalgamate writes what the Python script wrote, byte for byte, except for the time of generation. That line is gone so that the output is deterministic and --check can compare it. MODULE.bazel now pins the archives of third_party/ by version and sha256, so Bazel fetches and checks them instead of a tool of ours, and Renovate reads the versions there. Scripts nobody used are gone: fixWhitespace, updateDocumentSnippets, buildAndTest, and updateDocumentToC, which rewrote unrelated documents. CheckConvenienceHeaders leaves CTest, since //tools:check runs it in CI. The Python of tests/TestScripts stays: those are tests, not tools.
Diffstat (limited to 'tools/RepositoryReport.cpp')
-rw-r--r--tools/RepositoryReport.cpp276
1 files changed, 276 insertions, 0 deletions
diff --git a/tools/RepositoryReport.cpp b/tools/RepositoryReport.cpp
new file mode 100644
index 00000000..4b2b1502
--- /dev/null
+++ b/tools/RepositoryReport.cpp
@@ -0,0 +1,276 @@
+// Copyright (c) 2026 Progmasoft.
+// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
+
+#include <algorithm>
+#include <array>
+#include <cstdio>
+#include <cstdlib>
+#include <memory>
+#include <optional>
+#include <string_view>
+#include <system_error>
+
+#include "tools/Amalgamation.hpp"
+#include "tools/Repository.hpp"
+#include "tools/RepositoryReport.hpp"
+#include "tools/Versions.hpp"
+
+// yyjson, compiled here and nowhere else in the tools, like in the runner.
+#define yyjson_api static
+#include <yyjson.c>
+
+namespace Progmasoft::Catch3::Tools
+{
+ namespace
+ {
+ namespace fs = std::filesystem;
+
+#if defined(_WIN32)
+ constexpr char kPathSeparator = ';';
+ constexpr std::array<std::string_view, 4> kExtensions{ ".exe",
+ ".cmd",
+ ".bat",
+ ".com" };
+ constexpr std::string_view kNoErrors = " 2>nul";
+#else
+ constexpr char kPathSeparator = ':';
+ constexpr std::array<std::string_view, 1> kExtensions{ "" };
+ constexpr std::string_view kNoErrors = " 2>/dev/null";
+#endif
+
+ constexpr std::array<std::string_view, 11> kTools{
+ "git", "bazelisk", "cmake", "ninja",
+ "meson", "xmake", "premake5", "clang",
+ "clang-cl", "clang-format", "gcc",
+ };
+
+ constexpr std::string_view
+ OperatingSystem()
+ {
+#if defined(_WIN32)
+ return "windows";
+#elif defined(__APPLE__)
+ return "darwin";
+#elif defined(__linux__)
+ return "linux";
+#else
+ return "unknown";
+#endif
+ }
+
+ constexpr std::string_view
+ Architecture()
+ {
+#if defined(_M_X64) || defined(__x86_64__)
+ return "amd64";
+#elif defined(_M_ARM64) || defined(__aarch64__)
+ return "arm64";
+#else
+ return "unknown";
+#endif
+ }
+
+ [[nodiscard]] ToolInfo
+ Find(std::string_view name, std::string_view searchPath)
+ {
+ ToolInfo tool;
+ tool.Name = name;
+ while (!tool.Available && !searchPath.empty())
+ {
+ const auto end = searchPath.find(kPathSeparator);
+ const fs::path directory{ std::string{
+ searchPath.substr(0, end) } };
+ searchPath.remove_prefix(end == std::string_view::npos
+ ? searchPath.size()
+ : end + 1);
+ if (directory.empty())
+ {
+ continue;
+ }
+ for (const auto extension : kExtensions)
+ {
+ const auto file
+ = directory
+ / (std::string{ name } + std::string{ extension });
+ std::error_code error;
+ if (fs::is_regular_file(file, error))
+ {
+ tool.Path = file.string();
+ tool.Available = true;
+ break;
+ }
+ }
+ }
+ return tool;
+ }
+
+ /// What `git <arguments>` writes in `root`, or nothing when it fails.
+ [[nodiscard]] std::optional<std::string>
+ Git(const fs::path &root, std::string_view arguments)
+ {
+ const auto command = "git -C \"" + root.string() + "\" "
+ + std::string{ arguments }
+ + std::string{ kNoErrors };
+#if defined(_WIN32)
+ auto *pipe = _popen(command.c_str(), "r");
+#else
+ auto *pipe = popen(command.c_str(), "r");
+#endif
+ if (pipe == nullptr)
+ {
+ return std::nullopt;
+ }
+ std::string output;
+ std::array<char, 512> buffer{};
+ std::size_t count = 0;
+ while ((count = std::fread(buffer.data(), 1, buffer.size(), pipe))
+ != 0)
+ {
+ output.append(buffer.data(), count);
+ }
+#if defined(_WIN32)
+ const int status = _pclose(pipe);
+#else
+ const int status = pclose(pipe);
+#endif
+ if (status != 0)
+ {
+ return std::nullopt;
+ }
+ return std::string{ Trimmed(output) };
+ }
+
+ void
+ AddText(yyjson_mut_doc *document,
+ yyjson_mut_val *object,
+ const char *key,
+ const std::string &value)
+ {
+ if (!value.empty())
+ {
+ yyjson_mut_obj_add_strn(document,
+ object,
+ key,
+ value.data(),
+ value.size());
+ }
+ }
+
+ struct FreeDocument final
+ {
+ void
+ operator()(yyjson_mut_doc *document) const
+ {
+ yyjson_mut_doc_free(document);
+ }
+ };
+
+ struct FreeText final
+ {
+ void
+ operator()(char *text) const
+ {
+ std::free(text);
+ }
+ };
+ } // namespace
+
+ RepositoryReport
+ CollectRepositoryReport(const fs::path &root)
+ {
+ RepositoryReport report;
+ report.Root = root.string();
+ report.Os = OperatingSystem();
+ report.Architecture = Architecture();
+
+ Problems ignored;
+ report.Version = CurrentVersion(root, ignored).value_or("");
+ report.EngineVersion
+ = ParseEngineVersion(
+ ReadText(root / "src/Progmasoft/Catch3/EngineVersion.cpp")
+ .value_or(""))
+ .value_or("");
+
+ const auto searchPath = ReadEnvironment("PATH").value_or("");
+ for (const auto name : kTools)
+ {
+ report.Tools.push_back(Find(name, searchPath));
+ }
+
+ const auto branch = Git(root, "branch --show-current");
+ const auto revision = Git(root, "rev-parse HEAD");
+ const auto status
+ = Git(root, "status --porcelain --untracked-files=no");
+ if (branch && revision && status)
+ {
+ report.Branch = *branch;
+ report.Revision = *revision;
+ report.Dirty = !status->empty();
+ }
+ else
+ {
+ report.GitError = "Git checkout metadata could not be read";
+ }
+ return report;
+ }
+
+ std::string
+ FormatReport(const RepositoryReport &report)
+ {
+ std::string text = "Checkout: " + report.Root + "\n";
+ text += "Host: " + report.Os + "/" + report.Architecture + "\n";
+ text += "Catch3: " + report.Version + ", on the Catch2 "
+ + report.EngineVersion + " engine\n";
+ text += "Branch: " + report.Branch + "\n";
+ text += "Revision: " + report.Revision + "\n";
+ text += std::string{ "Tracked changes: " }
+ + (report.Dirty ? "true" : "false") + "\n";
+ if (!report.GitError.empty())
+ {
+ text += report.GitError + "\n";
+ }
+ for (const auto &tool : report.Tools)
+ {
+ auto name = tool.Name;
+ name.resize(std::max<std::size_t>(name.size(), 13), ' ');
+ text += name + (tool.Available ? tool.Path : "not found") + "\n";
+ }
+ return text;
+ }
+
+ std::string
+ ReportToJson(const RepositoryReport &report)
+ {
+ const std::unique_ptr<yyjson_mut_doc, FreeDocument> holder{
+ yyjson_mut_doc_new(nullptr)
+ };
+ auto *document = holder.get();
+ auto *root = yyjson_mut_obj(document);
+ yyjson_mut_doc_set_root(document, root);
+ AddText(document, root, "root", report.Root);
+ AddText(document, root, "os", report.Os);
+ AddText(document, root, "architecture", report.Architecture);
+ AddText(document, root, "version", report.Version);
+ AddText(document, root, "engineVersion", report.EngineVersion);
+ AddText(document, root, "branch", report.Branch);
+ AddText(document, root, "revision", report.Revision);
+ yyjson_mut_obj_add_bool(document, root, "dirty", report.Dirty);
+ AddText(document, root, "gitError", report.GitError);
+ auto *tools = yyjson_mut_obj_add_arr(document, root, "tools");
+ for (const auto &tool : report.Tools)
+ {
+ auto *entry = yyjson_mut_arr_add_obj(document, tools);
+ AddText(document, entry, "name", tool.Name);
+ AddText(document, entry, "path", tool.Path);
+ yyjson_mut_obj_add_bool(document,
+ entry,
+ "available",
+ tool.Available);
+ }
+ const std::unique_ptr<char, FreeText> text{
+ yyjson_mut_write(document, YYJSON_WRITE_PRETTY_TWO_SPACES, nullptr)
+ };
+ return text == nullptr ? std::string{}
+ : std::string{ text.get() } + "\n";
+ }
+} // namespace Progmasoft::Catch3::Tools