Add project files

This commit is contained in:
alejo committed 2026-10-24 09:53:48 -03:00
commit f2fe6b8855
114 files changed
+20525

No files matched your search

@@ -0,0 +1,15 @@
#pragma once
#include <string>
#include <vector>
namespace breeze::cli
{
/// CLIForge reads every token that starts with '-' as an option, so `--cargs -DBUILD_TESTS=ON`
/// would try to find an option called -D. The values of the pass-through options (--cargs,
/// --bargs and run's --args/-x) are rewritten to the `--cargs=-DBUILD_TESTS=ON` form CLIForge
/// does accept, each taking tokens up to the next option of the command.
std::vector<std::string> normalizeArguments(const std::vector<std::string>& args);
}
@@ -0,0 +1,55 @@
#pragma once
#include <filesystem>
#include <map>
#include <optional>
#include <string>
#include <vector>
#include "breeze/Presets.hpp"
#include "breeze/Project.hpp"
/// Building, installing, running and cleaning a project -- by driving the SDK's own scripts, so the
/// environment they set up (the pinned Ninja, the MSVC environment on Windows) is always the one
/// used. Where a request needs something the scripts don't offer (extra `cmake --build` arguments,
/// skipping the configure), the CLI runs `cmake --build` itself against the build the scripts made.
namespace breeze
{
struct BuildRequest
{
PresetQuery preset;
/// Release, Debug, ReleaseDebugSymbols or MinSizeRel (any case).
std::optional<std::string> mode;
std::optional<int> jobs;
std::vector<std::string> configureArgs;
std::vector<std::string> buildArgs;
bool noConfigure = false;
bool noBuild = false;
};
/// The CMake build type for a mode name; throws Error for an unknown one.
std::string buildTypeFor(const std::string& mode);
/// Values of the project's settings (BUILD_OUTPUT_DIR, DOCS_OUTPUT_DIR, ...) as the SDK
/// computes them. Falls back to the SDK's defaults for a legacy layout that can't be asked.
std::map<std::string, std::string> readConfigVariables(const Project& project,
const std::vector<std::string>& names);
/// The build directory for the preset (or the one built last), as the SDK defines it.
std::filesystem::path buildDirFor(const Project& project,
const std::optional<std::string>& preset);
/// Each returns the exit code of the command that did the work.
int build(const Project& project, const BuildRequest& request);
int install(const Project& project, const BuildRequest& request);
int docs(const Project& project);
int run(const Project& project, const std::optional<std::string>& id, const PresetQuery& where,
const std::vector<std::string>& programArgs);
int setup(const Project& project, bool required, bool all);
/// Removes the build directories (unless keepBuild) and the documentation (if alsoDocs).
/// Returns the paths removed, relative to the project.
std::vector<std::string> clean(const Project& project, bool keepBuild, bool alsoDocs);
}
@@ -0,0 +1,49 @@
#pragma once
#include <cstddef>
#include <functional>
#include <optional>
#include <string>
#include <vector>
/// Just enough of CMake's syntax to find and edit a command call in a CMakeLists.txt or a settings
/// file without disturbing anything else in it: comments, layout and line endings stay as they
/// were.
namespace breeze::cmake
{
/// One argument of a call. begin/end are byte offsets of the argument as written (quotes
/// included); text is its value without the quotes.
struct Token
{
std::size_t begin = 0;
std::size_t end = 0;
std::string text;
bool quoted = false;
};
/// A command call: name(args...). begin/end span the whole call, "name(" through ")".
struct Call
{
std::string name;
std::size_t begin = 0;
std::size_t end = 0;
std::vector<Token> args;
};
/// Every call in `text`, in order, whose name `wanted` accepts. Comments, quoted strings and
/// the arguments of other calls are never mistaken for calls.
std::vector<Call> findCalls(const std::string& text,
const std::function<bool(const std::string&)>& wanted);
/// The first set(<variable> ...) call.
std::optional<Call> findSet(const std::string& text, const std::string& variable);
/// `value` as a quoted CMake argument.
std::string quote(const std::string& value);
/// Replaces the token's text in `text` (offsets of later tokens are stale afterwards).
void replaceToken(std::string& text, const Token& token, const std::string& replacement);
}
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <filesystem>
#include <optional>
#include <string>
#include <vector>
#include "breeze/Version.hpp"
/// The CLI managing itself: which versions of it are installed, which one is picked, and running
/// the picked one. Versions live in <breeze home>/cli/<version>/; `cli/current` names the picked
/// one, and <breeze home>/bin/breeze is the copy to put on PATH. Whatever `breeze` the shell finds
/// hands the command to the picked version, so installing or picking never means changing PATH
/// again.
namespace breeze::self
{
/// The version of this program.
std::string version();
/// Installed versions, oldest first.
std::vector<Version> installed();
/// The version `cli pick` chose, if any.
std::optional<Version> picked();
/// Installs a version: this very program if that is its version, else a release of the
/// configured CLI source. The first version installed is picked.
void install(const std::string& version);
void pick(const std::string& version);
/// Removes a version; un-picks it if it was the picked one.
void uninstall(const std::string& version);
/// Prints the installed versions, marking the picked one and the running one.
void list();
/// If another version is picked, installed and runnable, runs it with `args` and does not
/// return. `cli ...` commands always run in the version that was started.
void forwardToPicked(const std::vector<std::string>& args);
}
@@ -0,0 +1,46 @@
#pragma once
#include <filesystem>
#include <optional>
#include <string>
#include <nlohmann/json.hpp>
/// The CLI's own, global state: where it lives and what the user configured.
namespace breeze
{
/// $BREEZE_HOME, or ~/.breeze. Holds config.json, the installed CLI versions (cli/), the SDK
/// snapshots (sdk/) and cached indexes.
std::filesystem::path breezeHome();
/// The keys of config.json. Each can also come from the environment (BREEZE_EDITOR,
/// BREEZE_SDK_SOURCE, BREEZE_CLI_SOURCE), which wins.
namespace config
{
inline constexpr const char* Editor = "editor";
inline constexpr const char* SdkSource = "sdkSource";
inline constexpr const char* CliSource = "cliSource";
}
class GlobalConfig
{
public:
/// Reads config.json (an absent file is an empty configuration).
static GlobalConfig load();
/// The environment's value for the key if set, else the file's.
[[nodiscard]] std::optional<std::string> effective(const std::string& key) const;
/// The file's value only.
[[nodiscard]] std::optional<std::string> stored(const std::string& key) const;
void set(const std::string& key, const std::string& value);
void erase(const std::string& key);
void save() const;
private:
nlohmann::json m_data = nlohmann::json::object();
};
}
@@ -0,0 +1,24 @@
#pragma once
#include <stdexcept>
#include <string>
namespace breeze
{
/// Every failure the CLI reports to the user: a message and the process exit code to end with.
class Error : public std::runtime_error
{
public:
explicit Error(const std::string& message, int exitCode = 1)
: std::runtime_error(message), m_exitCode(exitCode)
{
}
[[nodiscard]] int exitCode() const noexcept { return m_exitCode; }
private:
int m_exitCode;
};
}
+53
View File
@@ -0,0 +1,53 @@
#pragma once
#include <filesystem>
#include <optional>
#include <string>
#include <vector>
/// File system and host helpers. Paths are std::filesystem::path throughout; text files are read
/// and written as bytes, so the line endings a file has are the ones it keeps.
namespace breeze::fs
{
using std::filesystem::path;
/// Throws Error when the file can't be read.
std::string readText(const path& file);
std::optional<std::string> tryReadText(const path& file);
/// Creates the parent directories, then writes the file (replacing it).
void writeText(const path& file, const std::string& text);
/// Copies `from` into `to` (merging into what is there, overwriting files). Entries whose name
/// is in `skipNames` are left out at every level.
void copyTree(const path& from, const path& to, const std::vector<std::string>& skipNames = {});
/// Removes the file or directory tree if it exists. Throws Error on failure.
void removeTree(const path& p);
/// A new, empty directory under the system's temp directory.
path makeTempDir(const std::string& prefix);
/// The user's home directory (HOME, or USERPROFILE on Windows).
path homeDir();
/// Where the running program's executable is.
path executablePath();
/// "linux", "macos" or "windows".
std::string hostPlatform();
/// "x64", "arm64", "arm32" or "x86".
std::string hostArch();
/// chmod +x on POSIX; nothing on Windows.
void makeExecutable(const path& file);
/// True for a regular file that can be run.
bool isExecutable(const path& file);
/// The path with forward slashes, for messages that read the same on every OS.
std::string generic(const path& p);
}
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include <filesystem>
#include <string>
#include <vector>
#include "breeze/Version.hpp"
/// The little of git the CLI needs: listing the version tags of a repository and exporting one
/// tag's files. Repositories are URLs or local paths; git itself must be installed.
namespace breeze::git
{
struct Tag
{
/// The tag as written in the repository, e.g. "v1.2.0".
std::string name;
Version version;
};
/// Is `source` something git can clone (a URL, or a local repository) rather than a plain
/// directory?
bool isRepository(const std::string& source);
/// The tags of the repository that are versions, in no particular order. Throws Error when the
/// repository can't be reached.
std::vector<Tag> versionTags(const std::string& repository);
/// Exports the files of `tag` (the default branch's tip when empty) into `destination`, without
/// the repository's own history.
void exportTree(const std::string& repository, const std::string& tag,
const std::filesystem::path& destination);
}
@@ -0,0 +1,97 @@
#pragma once
#include <filesystem>
#include <optional>
#include <string>
#include <vector>
#include "breeze/Project.hpp"
/// A project's modules: the directories under source/ (and source/tests/, source/examples/) that
/// each register one CMake target with one of the SDK's add_*_module macros. They are read from the
/// CMakeLists.txt files themselves, so what the CLI shows is what CMake will do, and the files it
/// writes are the ones a person would have written.
namespace breeze
{
enum class ModuleKind
{
Exec,
Lib,
Obj,
Test,
Example
};
enum class LibType
{
Unspecified,
Static,
Shared
};
/// "exec", "lib", "obj", "test" or "example".
std::string kindName(ModuleKind kind);
struct Module
{
/// The CMake target: what DEPENDS, `breeze run` and `breeze deps` call it.
std::string id;
/// The directory's name.
std::string dirName;
std::filesystem::path dir;
ModuleKind kind = ModuleKind::Exec;
LibType libType = LibType::Unspecified;
std::string outputName;
std::vector<std::string> deps;
/// The macro that registered it, e.g. add_lib_module.
std::string macro;
};
/// Every module, main ones first (by directory name), then tests, then examples.
std::vector<Module> scanModules(const Project& project);
/// By id, else by directory name. Null when there is no such module.
const Module* findModule(const std::vector<Module>& modules, const std::string& idOrDir);
struct NewModule
{
ModuleKind kind = ModuleKind::Exec;
std::string name;
std::optional<std::string> id;
std::vector<std::string> deps;
LibType libType = LibType::Unspecified;
};
/// Creates the module's directory, CMakeLists.txt and starter files. Throws Error on anything
/// it would have to guess about or overwrite.
Module addModule(const Project& project, const NewModule& request);
/// Removes the module's directory. Refuses while another module still depends on it.
void deleteModule(const Project& project, const std::string& idOrDir);
struct RenameResult
{
std::string oldId;
std::string newId;
std::string oldDir;
std::string newDir;
/// Modules whose CMakeLists.txt had a DEPENDS entry updated.
std::vector<std::string> updatedDependents;
};
/// Renames the directory and/or the id, and rewrites DEPENDS entries elsewhere to the new id.
RenameResult renameModule(const Project& project, const std::string& idOrDir,
const std::optional<std::string>& newName,
const std::optional<std::string>& newId);
/// What `module` depends on, without repeats, in the order met (nearest first). Entries that
/// are not modules of the project (fmt::fmt, pthread) are included as they were written.
std::vector<std::string> dependenciesOf(const std::vector<Module>& modules,
const Module& module, bool recursive);
/// The modules that depend on `module`, directly or (recursive) through others.
std::vector<std::string> dependentsOf(const std::vector<Module>& modules, const Module& module,
bool recursive);
}
@@ -0,0 +1,45 @@
#pragma once
#include <optional>
#include <string>
#include <vector>
#include "breeze/Project.hpp"
/// The project's CMake presets, which are what a platform, an architecture and a compiler mean:
/// every SDK names its configure presets "<platform>-<arch>-<compiler>" (linux-x64-gcc,
/// windows-x64-clang-cl).
namespace breeze
{
struct Preset
{
std::string name;
std::string description;
std::string platform; // linux, macos, windows
std::string arch; // x64, x86, arm32, arm64
std::string compiler; // the rest of the name: gcc, clang, msvc, clang-cl, mingw64, ...
};
/// The configure presets usable on this machine (what `cmake --list-presets` shows).
std::vector<Preset> listPresets(const Project& project);
/// What was asked for; every field is optional and compared ignoring case and punctuation
/// ("ClangCl" finds clang-cl).
struct PresetQuery
{
std::optional<std::string> preset;
std::optional<std::string> platform;
std::optional<std::string> arch;
std::optional<std::string> compiler;
};
/// The preset a command should use: the named preset; else one made from the platform,
/// architecture and compiler given, the project's defaults and then this machine for whatever
/// is missing; else the project's default preset; else none (the SDK's own default). Throws an
/// Error that lists the choices when what was asked for doesn't exist. `note` gets a line to
/// show the user when the choice involved a guess.
std::optional<std::string> resolvePreset(const Project& project, const Defaults& defaults,
const PresetQuery& query, std::string* note = nullptr);
}
@@ -0,0 +1,56 @@
#pragma once
#include <filesystem>
#include <optional>
#include <string>
#include <utility>
#include <vector>
/// Running other programs. Arguments are passed as a vector, never through a shell, so nothing
/// needs quoting by the caller.
namespace breeze::proc
{
struct Options
{
/// Working directory; empty keeps the current one.
std::filesystem::path cwd;
/// Variables set (or replaced) for the child only.
std::vector<std::pair<std::string, std::string>> env;
/// Collect the child's standard output into Result::output instead of showing it.
bool capture = false;
/// With capture, standard error is collected too (merged into the same string).
bool mergeStderr = false;
/// Throw the child's output away.
bool silent = false;
};
struct Result
{
int exitCode = 0;
std::string output;
};
/// Runs argv[0] with the rest as arguments (looked up on PATH when it has no directory part)
/// and waits. A program that can't be started is exit code 127. Throws Error only for failures
/// of this function itself (nothing to run, the system refusing to create a process).
Result run(const std::vector<std::string>& argv, const Options& options = {});
/// Runs a .bat/.cmd script on Windows through cmd.exe, or the script itself elsewhere.
Result runScript(const std::filesystem::path& script, const std::vector<std::string>& args,
const Options& options = {});
/// Replaces this process with argv (POSIX), or runs it and exits with its status (Windows).
/// Does not return. Throws Error when it can't be started.
[[noreturn]] void execReplace(const std::vector<std::string>& argv);
/// The first match for `program` on PATH, if any.
std::optional<std::filesystem::path> findInPath(const std::string& program);
/// Sets an environment variable for this process and the programs it starts from now on.
void setEnvironment(const std::string& name, const std::string& value);
bool stdinIsTerminal();
bool stdoutIsTerminal();
}
@@ -0,0 +1,109 @@
#pragma once
#include <filesystem>
#include <optional>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
#include "breeze/Version.hpp"
/// Finding a breeze project, telling which SDK it has, and reading and writing the project's own
/// settings.
///
/// The CLI is global and projects carry their SDK (the template's breeze/ folder), so each project
/// may be on a different SDK version -- or on the layout before breeze/ existed, with scripts/ and
/// cmake/ at the root. Everything here asks the project's Sdk where things are instead of assuming.
namespace breeze
{
enum class SdkLayout
{
/// breeze/ (the SDK) and project/ (the project's settings).
Breeze,
/// The flat layout that came before: scripts/, cmake/ and Configuration.cmake at the root.
Legacy
};
struct Sdk
{
SdkLayout layout = SdkLayout::Breeze;
/// breeze/VERSION. Empty for the legacy layout and for a breeze/ that predates the file.
std::optional<Version> version;
/// <root>/breeze, or <root> in the legacy layout.
std::filesystem::path dir;
std::filesystem::path scriptsDir;
std::filesystem::path cmakeDir;
/// project/Configuration.cmake, or Configuration.cmake in the legacy layout.
std::filesystem::path settingsFile;
/// Does this SDK have add_objects_module?
bool supportsObjects = false;
/// "1.0.0", "unversioned (breeze/ layout)" or "legacy (flat layout, unversioned)".
[[nodiscard]] std::string describe() const;
};
struct Project
{
std::filesystem::path root;
Sdk sdk;
/// <root>/.breeze: what the CLI keeps in the project (defaults, installed tools).
[[nodiscard]] std::filesystem::path metaDir() const;
[[nodiscard]] std::filesystem::path defaultsFile() const;
[[nodiscard]] std::filesystem::path toolsDir() const;
/// The SDK's script `name` for this OS: scripts/<name>.sh, or .bat on Windows.
[[nodiscard]] std::filesystem::path script(const std::string& name) const;
};
/// Does `root` hold a breeze project, and with which SDK?
std::optional<Sdk> detectSdk(const std::filesystem::path& root);
/// The nearest project at or above `start`.
std::optional<Project> findProject(const std::filesystem::path& start);
/// findProject, or an Error explaining that the command needs a project.
Project requireProject(const std::filesystem::path& start = std::filesystem::current_path());
enum class Setting
{
Name, // CMake's project() name
Version, // the project's own version, major.minor[.patch[.tweak]]
Id // the machine-friendly identifier (PROJECT_CODE_NAME)
};
std::string readSetting(const Project& project, Setting which);
/// Validates the value, then changes just that value in the file, leaving everything else as it
/// is.
void writeSetting(const Project& project, Setting which, const std::string& value);
/// The CLI's per-project defaults, in .breeze/defaults.json (meant to be committed).
class Defaults
{
public:
static Defaults load(const Project& project);
/// key: platform, arch, compiler, preset or mode.
[[nodiscard]] std::optional<std::string> build(const std::string& key) const;
void setBuild(const std::string& key, const std::string& value);
void clearBuild(const std::string& key);
/// The module id `breeze run` runs when it is given none.
[[nodiscard]] std::optional<std::string> run() const;
void setRun(const std::string& id);
void clearRun();
void save(const Project& project) const;
private:
nlohmann::json m_data = nlohmann::json::object();
};
}
+69
View File
@@ -0,0 +1,69 @@
#pragma once
#include <filesystem>
#include <optional>
#include <string>
#include <vector>
#include "breeze/Project.hpp"
#include "breeze/Version.hpp"
/// SDK versions, where they come from and what moving a project between them means.
///
/// An SDK version is a snapshot of the whole template: breeze/ (the SDK proper, replaced as a whole
/// on a change), plus the files the project owns and starts from (project/Configuration.cmake, the
/// root CMakeLists.txt, CMakePresets.json, the dot files). Versions come from a *source*, either a
/// git repository whose tags are versions (v1.2.0, 1.2.0) or a directory with one subdirectory per
/// version.
namespace breeze::sdk
{
class Source
{
public:
struct Entry
{
Version version;
/// The tag, or the directory name, it was found under.
std::string ref;
};
/// The configured source (`breeze config sdk-source`, or BREEZE_SDK_SOURCE); throws Error
/// when there is none.
static Source configured();
explicit Source(std::string location);
[[nodiscard]] const std::string& location() const { return m_location; }
/// All versions, oldest first.
[[nodiscard]] std::vector<Entry> versions() const;
/// The files of one version, fetched into the CLI's cache on first use.
[[nodiscard]] std::filesystem::path snapshot(const Entry& entry) const;
/// "latest" (or nothing), or a version number: the matching entry. Throws Error, listing
/// what exists, when there is no such version.
[[nodiscard]] Entry resolve(const std::optional<std::string>& wanted) const;
private:
std::string m_location;
bool m_isRepository = false;
};
/// Creates ./<name> from an SDK version: a project with that SDK, the settings set to the new
/// project's name and id, no demo modules, and one executable module named after the project.
void createProject(const std::string& name, const std::optional<std::string>& id,
const std::optional<std::string>& sdkVersion);
/// Reports which SDK versions exist and whether the project is behind.
void update(const Project& project);
/// Moves the project to the newest version with the same major as its own.
void upgrade(const Project& project);
/// Moves the project to exactly this version, whichever way and across majors, and a legacy
/// (flat) project to the breeze/ layout.
void migrate(const Project& project, const std::string& version);
}
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <string>
#include <string_view>
#include <vector>
/// Small string helpers; nothing here knows about breeze.
namespace breeze::text
{
std::string trim(std::string_view s);
std::vector<std::string> split(std::string_view s, char delimiter);
std::vector<std::string> splitLines(std::string_view s);
std::string join(const std::vector<std::string>& parts, std::string_view separator);
std::string lower(std::string_view s);
bool startsWith(std::string_view s, std::string_view prefix);
bool endsWith(std::string_view s, std::string_view suffix);
std::string replaceAll(std::string s, std::string_view from, std::string_view to);
/// "CoreFarewell" -> "core_farewell", "HTTPServer" -> "http_server", "my-app" -> "my_app".
std::string snakeCase(std::string_view s);
/// Lower-case letters and digits only: "Clang-CL" -> "clangcl". For comparing names loosely.
std::string normalized(std::string_view s);
/// A module or project name: a letter, then letters, digits, '_' (and '-' when allowDash).
bool isName(std::string_view s, bool allowDash = false);
/// A CMake target id as this project writes them: letters, digits, '_', '.', '+', '-' and "::".
bool isTargetId(std::string_view s);
/// "x" for a version-less message, with a '<none>' stand-in for an empty value.
std::string orNone(std::string_view s);
}
@@ -0,0 +1,57 @@
#pragma once
#include <optional>
#include <string>
#include <vector>
#include "breeze/Project.hpp"
/// Tools: extensions a project installs from a repository. A tool is a git repository (or, while it
/// is being written, a directory) with a `breeze-tool.json` at its root:
///
/// { "id": "resembed", "version": "1.0.0", "description": "...", "entry": "bin/resembed.sh" }
///
/// `entry` may instead be { "posix": "...", "windows": "..." }. Installed, a tool is copied into
/// <project>/.breeze/tools/<id>/ and recorded in .breeze/tools.json (meant to be committed). From
/// then on `breeze tool <id> <arguments...>` runs its entry with those arguments, in the project's
/// directory, with the project described in environment variables (BREEZE_PROJECT_ROOT,
/// BREEZE_SDK_DIR, BREEZE_SDK_VERSION, BREEZE_TOOL_ID, BREEZE_TOOL_DIR, BREEZE_CLI, BREEZE_EDITOR).
/// What commands a tool offers, and what they do, is entirely up to the tool.
namespace breeze::tools
{
struct Tool
{
std::string id;
std::string version;
std::string repo;
std::string description;
std::filesystem::path dir;
};
/// The words after `breeze tool` that belong to the CLI itself and can't be tool ids.
bool isReservedName(const std::string& word);
std::vector<Tool> installed(const Project& project);
/// Installs the newest version of the tool in `repo`.
void install(const Project& project, const std::string& repo);
void uninstall(const Project& project, const std::string& id);
/// Reports whether newer versions of the tool exist.
void update(const Project& project, const std::string& id);
/// Moves the tool to its newest version with the same major version.
void upgrade(const Project& project, const std::string& id);
/// Moves the tool to exactly this version.
void migrate(const Project& project, const std::string& id, const std::string& version);
void list(const Project& project);
/// Runs the tool's entry with `args`; the entry's exit code, or nullopt when no such tool is
/// installed.
std::optional<int> run(const Project& project, const std::string& id,
const std::vector<std::string>& args);
}
@@ -0,0 +1,35 @@
#pragma once
#include <optional>
#include <string>
#include <string_view>
#include <vector>
namespace breeze
{
/// A semantic version: major.minor.patch with an optional pre-release tag.
/// A pre-release sorts before the release it leads up to (1.0.0-rc1 < 1.0.0).
struct Version
{
int major = 0;
int minor = 0;
int patch = 0;
std::string pre;
[[nodiscard]] std::string toString() const;
};
/// Accepts "1.2.3", "v1.2.3", "1.2.3-rc1", "1.2" (patch 0) and "1" (minor and patch 0);
/// anything else, including build metadata after '+', is dropped or rejected.
std::optional<Version> parseVersion(std::string_view text);
int compare(const Version& a, const Version& b);
bool operator<(const Version& a, const Version& b);
bool operator==(const Version& a, const Version& b);
/// The greatest of `versions`; with `sameMajorAs`, only of those with that major version.
std::optional<Version> greatest(const std::vector<Version>& versions,
std::optional<int> sameMajorAs = std::nullopt);
}