4#include "absl/status/status.h"
5#include "absl/status/statusor.h"
14absl::Status BuildToolUnavailableStatus() {
15 return absl::FailedPreconditionError(
16 "Build tools are not available in Web/WASM builds");
27 return BuildToolUnavailableStatus();
32 return BuildToolUnavailableStatus();
36 const std::string&,
const std::string&) {
37 return BuildToolUnavailableStatus();
43 status.progress_percent = -1;
44 status.last_result_summary =
45 "Build tools are not available in Web/WASM builds";
50 return BuildToolUnavailableStatus();
66 return BuildToolUnavailableStatus();
70 const std::string&,
const std::string&) {
71 return BuildToolUnavailableStatus();
75 const std::string&,
const std::chrono::seconds&) {
76 return BuildToolUnavailableStatus();
98 const resources::ArgumentParser& parser) {
99 if (!parser.GetString(
"preset").has_value()) {
100 return absl::InvalidArgumentError(
"--preset is required");
102 return absl::OkStatus();
106 Rom*,
const resources::ArgumentParser&, resources::OutputFormatter&) {
107 return BuildToolUnavailableStatus();
111 const resources::ArgumentParser&) {
112 return absl::OkStatus();
116 Rom*,
const resources::ArgumentParser&, resources::OutputFormatter&) {
117 return BuildToolUnavailableStatus();
121 const resources::ArgumentParser&) {
122 return absl::OkStatus();
126 Rom*,
const resources::ArgumentParser&, resources::OutputFormatter&) {
127 return BuildToolUnavailableStatus();
131 const resources::ArgumentParser&) {
132 return absl::OkStatus();
136 Rom*,
const resources::ArgumentParser&, resources::OutputFormatter& formatter) {
137 formatter.BeginObject(
"Build Status");
138 formatter.AddField(
"available",
false);
139 formatter.AddField(
"message",
140 "Build tools are not available in Web/WASM builds");
141 formatter.EndObject();
142 return absl::OkStatus();
162#include "absl/strings/match.h"
163#include "absl/strings/str_cat.h"
164#include "absl/strings/str_format.h"
165#include "absl/strings/str_join.h"
166#include "absl/strings/str_split.h"
183namespace fs = std::filesystem;
191 const char* env_build_dir = std::getenv(
"YAZE_BUILD_DIR");
192 if (env_build_dir !=
nullptr && env_build_dir[0] !=
'\0') {
200 if (build_path.is_relative()) {
223 const std::string& preset) {
224 if (preset.empty()) {
225 return absl::InvalidArgumentError(
"Preset name cannot be empty");
231 return absl::InvalidArgumentError(
232 absl::StrFormat(
"Invalid preset '%s'. Available presets: %s", preset,
233 absl::StrJoin(available,
", ")));
240 return absl::InternalError(
241 absl::StrFormat(
"Failed to create build directory: %s", ec.message()));
245 std::string command = absl::StrFormat(
"cmake --preset %s -B \"%s\"", preset,
249 command +=
" --debug-output";
253 command, absl::StrFormat(
"Configuring with preset '%s'", preset));
257 const std::string& target,
const std::string& config) {
261 return absl::FailedPreconditionError(absl::StrFormat(
262 "Build directory '%s' not configured. Run Configure first.",
267 std::string command =
270 if (!config.empty()) {
271 command += absl::StrFormat(
" --config %s", config);
274 if (!target.empty()) {
275 command += absl::StrFormat(
" --target %s", target);
279 command +=
" --parallel";
282 command +=
" --verbose";
287 absl::StrFormat(
"Building %s", target.empty() ?
"all targets" : target));
291 const std::string& filter,
const std::string& rom_path) {
295 return absl::FailedPreconditionError(absl::StrFormat(
296 "Build directory '%s' not configured. Run Configure first.",
301 std::string command = absl::StrFormat(
305 if (!filter.empty()) {
307 if (filter ==
"unit" || filter ==
"integration" || filter ==
"e2e" ||
308 filter ==
"stable" || filter ==
"experimental" ||
309 filter ==
"rom_dependent") {
310 command += absl::StrFormat(
" -L %s", filter);
313 command += absl::StrFormat(
" -R \"%s\"", filter);
318 std::string env_setup;
319 if (!rom_path.empty()) {
320 if (!fs::exists(rom_path)) {
321 return absl::NotFoundError(
322 absl::StrFormat(
"ROM file not found: %s", rom_path));
325 env_setup = absl::StrFormat(
326 "set YAZE_TEST_ROM_VANILLA=\"%s\" && set YAZE_TEST_ROM_PATH=\"%s\" && ",
329 env_setup = absl::StrFormat(
330 "YAZE_TEST_ROM_VANILLA=\"%s\" YAZE_TEST_ROM_PATH=\"%s\" ", rom_path,
336 command +=
" --parallel";
339 command +=
" --verbose";
342 std::string full_command = env_setup + command;
348 filter.empty() ?
"" : absl::StrFormat(
" (filter: %s)", filter)));
362 "Last operation: %s (exit code: %d, duration: %lds)",
377 result.
output =
"Build directory does not exist, nothing to clean";
379 result.
duration = std::chrono::seconds(0);
384 std::string command = absl::StrFormat(
"cmake --build \"%s\" --target clean",
394 return fs::exists(build_path) && fs::exists(build_path /
"CMakeCache.txt");
413 return absl::OkStatus();
421 const std::string& command,
const std::string& operation_name) {
425 return absl::UnavailableError(
"Another build operation is in progress");
430 auto start_time = std::chrono::steady_clock::now();
436 auto end_time = std::chrono::steady_clock::now();
438 std::chrono::duration_cast<std::chrono::seconds>(end_time - start_time);
441 auto& build_result = *result;
442 build_result.duration = duration;
443 build_result.command_executed = command;
460 const std::string& command,
const std::chrono::seconds& timeout) {
468 fs::path original_dir = fs::current_path();
472 fs::current_path(project_root, ec);
474 return absl::InternalError(
475 absl::StrFormat(
"Failed to change directory: %s", ec.message()));
481 std::string full_command = absl::StrFormat(
"cmd /c %s 2>&1", command);
482 FILE* pipe = _popen(full_command.c_str(),
"r");
485 std::string full_command = command +
" 2>&1";
486 FILE* pipe = popen(full_command.c_str(),
"r");
490 fs::current_path(original_dir, ec);
491 return absl::InternalError(
"Failed to execute command");
495 std::stringstream output_stream;
496 std::stringstream error_stream;
497 std::array<char, 4096> buffer;
498 size_t total_output = 0;
499 auto start_time = std::chrono::steady_clock::now();
503 int pipe_fd = fileno(pipe);
504 int flags = fcntl(pipe_fd, F_GETFL, 0);
505 fcntl(pipe_fd, F_SETFL, flags | O_NONBLOCK);
510 auto current_time = std::chrono::steady_clock::now();
511 auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
512 current_time - start_time);
514 if (elapsed >= timeout) {
521 fs::current_path(original_dir, ec);
522 return absl::DeadlineExceededError(absl::StrFormat(
523 "Command timed out after %ld seconds", timeout.count()));
527 if (fgets(buffer.data(), buffer.size(), pipe) !=
nullptr) {
528 size_t len = strlen(buffer.data());
534 output_stream << buffer.data();
537 std::string line(buffer.data());
538 std::transform(line.begin(), line.end(), line.begin(), ::tolower);
539 if (line.find(
"error") != std::string::npos ||
540 line.find(
"failed") != std::string::npos ||
541 line.find(
"fatal") != std::string::npos) {
542 error_stream << buffer.data();
552 if (errno == EAGAIN || errno == EWOULDBLOCK) {
553 std::this_thread::sleep_for(std::chrono::milliseconds(100));
566 int status = pclose(pipe);
567 if (WIFEXITED(status)) {
569 }
else if (WIFSIGNALED(status)) {
570 result.
exit_code = 128 + WTERMSIG(status);
577 fs::current_path(original_dir, ec);
581 result.
output = output_stream.str();
586 return absl::CancelledError(
"Operation was cancelled");
594 fs::path current = fs::current_path();
595 fs::path root = current;
598 while (!root.empty() && root != root.root_path()) {
600 if (fs::exists(root /
"CMakeLists.txt") &&
601 fs::exists(root /
"src" /
"yaze.cc") &&
602 fs::exists(root /
"CMakePresets.json")) {
603 return root.string();
606 if (fs::exists(root /
".git")) {
608 if (fs::exists(root /
"src" /
"cli") &&
609 fs::exists(root /
"src" /
"app")) {
610 return root.string();
613 root = root.parent_path();
617 return current.string();
623#elif defined(__APPLE__)
625#elif defined(__linux__)
633 std::vector<std::string> presets;
635 fs::path presets_file = fs::path(
GetProjectRoot()) /
"CMakePresets.json";
636 if (!fs::exists(presets_file)) {
641 std::ifstream file(presets_file);
646 std::stringstream buffer;
647 buffer << file.rdbuf();
648 std::string content = buffer.str();
655 bool in_configure_presets =
false;
656 bool in_preset_object =
false;
657 bool is_hidden =
false;
658 std::string current_preset_name;
661 std::regex configure_presets_regex(
"\"configurePresets\"\\s*:\\s*\\[");
662 std::regex preset_name_regex(
"\"name\"\\s*:\\s*\"([^\"]+)\"");
663 std::regex hidden_regex(
"\"hidden\"\\s*:\\s*true");
664 std::regex condition_regex(
"\"condition\"\\s*:");
666 std::istringstream stream(content);
670 while (std::getline(stream, line)) {
672 if (std::regex_search(line, configure_presets_regex)) {
673 in_configure_presets =
true;
677 if (!in_configure_presets)
681 for (
char c : line) {
684 if (brace_count == 1) {
685 in_preset_object =
true;
687 current_preset_name.clear();
689 }
else if (c ==
'}') {
691 if (brace_count == 0 && in_preset_object) {
693 if (!current_preset_name.empty() && !is_hidden) {
695 bool include =
false;
697 if (platform ==
"Windows") {
699 if (absl::StartsWith(current_preset_name,
"win-") ||
700 absl::StartsWith(current_preset_name,
"ci-windows") ||
701 (!absl::StartsWith(current_preset_name,
"mac-") &&
702 !absl::StartsWith(current_preset_name,
"lin-") &&
703 !absl::StartsWith(current_preset_name,
"ci-"))) {
706 }
else if (platform ==
"Darwin") {
708 if (absl::StartsWith(current_preset_name,
"mac-") ||
709 absl::StartsWith(current_preset_name,
"ci-macos") ||
710 (!absl::StartsWith(current_preset_name,
"win-") &&
711 !absl::StartsWith(current_preset_name,
"lin-") &&
712 !absl::StartsWith(current_preset_name,
"ci-"))) {
715 }
else if (platform ==
"Linux") {
717 if (absl::StartsWith(current_preset_name,
"lin-") ||
718 absl::StartsWith(current_preset_name,
"ci-linux") ||
719 (!absl::StartsWith(current_preset_name,
"win-") &&
720 !absl::StartsWith(current_preset_name,
"mac-") &&
721 !absl::StartsWith(current_preset_name,
"ci-"))) {
727 presets.push_back(current_preset_name);
730 in_preset_object =
false;
732 }
else if (c ==
']' && brace_count == -1) {
734 in_configure_presets =
false;
739 if (in_preset_object) {
742 if (std::regex_search(line, match, preset_name_regex)) {
743 current_preset_name = match[1].str();
747 if (std::regex_search(line, hidden_regex)) {
754 std::sort(presets.begin(), presets.end());
761 return std::find(available.begin(), available.end(), preset) !=
780 if (!parser.
GetString(
"preset").has_value()) {
781 return absl::InvalidArgumentError(
"--preset is required");
783 return absl::OkStatus();
791 std::string preset = parser.
GetString(
"preset").value();
792 std::string build_dir = parser.
GetString(
"build-dir").value_or(
"build_ai");
793 bool verbose = parser.
HasFlag(
"verbose");
806 return result.status();
811 formatter.
AddField(
"preset", preset);
813 formatter.
AddField(
"success", result->success ?
"true" :
"false");
814 formatter.
AddField(
"exit_code", std::to_string(result->exit_code));
816 absl::StrFormat(
"%ld seconds", result->duration.count()));
818 if (!result->output.empty()) {
820 const size_t max_lines = 100;
821 std::vector<std::string> lines = absl::StrSplit(result->output,
'\n');
822 if (lines.size() > max_lines) {
823 std::vector<std::string> truncated(lines.end() - max_lines, lines.end());
825 absl::StrFormat(
"[...truncated %zu lines...]\n%s",
826 lines.size() - max_lines,
827 absl::StrJoin(truncated,
"\n")));
829 formatter.
AddField(
"output", result->output);
833 if (!result->error_output.empty()) {
834 formatter.
AddField(
"errors", result->error_output);
839 if (!result->success) {
840 return absl::InternalError(
"Configuration failed");
843 return absl::OkStatus();
849 return absl::OkStatus();
857 std::string target = parser.
GetString(
"target").value_or(
"");
858 std::string config = parser.
GetString(
"config").value_or(
"");
859 std::string build_dir = parser.
GetString(
"build-dir").value_or(
"build_ai");
860 bool verbose = parser.
HasFlag(
"verbose");
868 build_tool_ = std::make_unique<BuildTool>(tool_config);
873 return result.status();
878 formatter.
AddField(
"target", target.empty() ?
"all" : target);
879 if (!config.empty()) {
880 formatter.
AddField(
"configuration", config);
882 formatter.
AddField(
"build_directory", build_dir);
883 formatter.
AddField(
"success", result->success ?
"true" :
"false");
884 formatter.
AddField(
"exit_code", std::to_string(result->exit_code));
886 absl::StrFormat(
"%ld seconds", result->duration.count()));
889 if (!result->output.empty()) {
890 const size_t max_lines = 100;
891 std::vector<std::string> lines = absl::StrSplit(result->output,
'\n');
892 if (lines.size() > max_lines) {
893 std::vector<std::string> truncated(lines.end() - max_lines, lines.end());
895 absl::StrFormat(
"[...truncated %zu lines...]\n%s",
896 lines.size() - max_lines,
897 absl::StrJoin(truncated,
"\n")));
898 formatter.
AddField(
"output_truncated",
"true");
900 formatter.
AddField(
"output", result->output);
904 if (!result->error_output.empty()) {
905 formatter.
AddField(
"errors", result->error_output);
910 if (!result->success) {
911 return absl::InternalError(
"Build failed");
914 return absl::OkStatus();
920 return absl::OkStatus();
928 std::string filter = parser.
GetString(
"filter").value_or(
"");
929 std::string rom_path = parser.
GetString(
"rom-path").value_or(
"");
930 std::string build_dir = parser.
GetString(
"build-dir").value_or(
"build_ai");
931 bool verbose = parser.
HasFlag(
"verbose");
938 config.
timeout = std::chrono::seconds(300);
943 auto result =
build_tool_->RunTests(filter, rom_path);
945 return result.status();
950 if (!filter.empty()) {
951 formatter.
AddField(
"filter", filter);
953 if (!rom_path.empty()) {
954 formatter.
AddField(
"rom_path", rom_path);
956 formatter.
AddField(
"build_directory", build_dir);
957 formatter.
AddField(
"success", result->success ?
"true" :
"false");
958 formatter.
AddField(
"exit_code", std::to_string(result->exit_code));
960 absl::StrFormat(
"%ld seconds", result->duration.count()));
963 if (!result->output.empty()) {
965 std::regex summary_regex(
966 R
"((\d+)% tests passed, (\d+) tests failed out of (\d+))");
968 if (std::regex_search(result->output, match, summary_regex)) {
969 formatter.
AddField(
"tests_total", match[3].str());
970 formatter.
AddField(
"tests_failed", match[2].str());
971 formatter.
AddField(
"pass_rate", match[1].str() +
"%");
975 const size_t max_lines = 50;
976 std::vector<std::string> lines = absl::StrSplit(result->output,
'\n');
977 if (lines.size() > max_lines) {
978 std::vector<std::string> truncated(lines.end() - max_lines, lines.end());
980 absl::StrFormat(
"[...truncated %zu lines...]\n%s",
981 lines.size() - max_lines,
982 absl::StrJoin(truncated,
"\n")));
984 formatter.
AddField(
"output", result->output);
988 if (!result->error_output.empty()) {
989 formatter.
AddField(
"errors", result->error_output);
994 if (!result->success) {
995 return absl::InternalError(
"Tests failed");
998 return absl::OkStatus();
1004 return absl::OkStatus();
1011 std::string build_dir = parser.
GetString(
"build-dir").value_or(
"build_ai");
1017 build_tool_ = std::make_unique<BuildTool>(config);
1023 formatter.
AddField(
"build_directory", build_dir);
1024 formatter.
AddField(
"directory_ready",
1025 build_tool_->IsBuildDirectoryReady() ?
"true" :
"false");
1026 formatter.
AddField(
"operation_running", status.is_running ?
"true" :
"false");
1028 if (status.is_running) {
1029 formatter.
AddField(
"current_operation", status.current_operation);
1032 auto now = std::chrono::system_clock::now();
1033 auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
1034 now - status.start_time);
1036 absl::StrFormat(
"%ld seconds", elapsed.count()));
1039 if (!status.last_result_summary.empty()) {
1040 formatter.
AddField(
"last_result", status.last_result_summary);
1044 auto presets =
build_tool_->ListAvailablePresets();
1045 if (!presets.empty()) {
1047 for (
const auto& preset : presets) {
1055 if (last_result.has_value()) {
1057 formatter.
AddField(
"command", last_result->command_executed);
1058 formatter.
AddField(
"success", last_result->success ?
"true" :
"false");
1059 formatter.
AddField(
"exit_code", std::to_string(last_result->exit_code));
1062 absl::StrFormat(
"%ld seconds", last_result->duration.count()));
1068 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Utility for parsing common CLI argument patterns.
std::optional< std::string > GetString(const std::string &name) const
Parse a named argument (e.g., –format=json or –format json)
bool HasFlag(const std::string &name) const
Check if a flag is present.