13#include "absl/strings/match.h"
14#include "absl/strings/str_cat.h"
15#include "absl/strings/str_format.h"
16#include "absl/strings/str_join.h"
17#include "absl/strings/str_split.h"
34namespace fs = std::filesystem;
42 const char* env_build_dir = std::getenv(
"YAZE_BUILD_DIR");
43 if (env_build_dir !=
nullptr && env_build_dir[0] !=
'\0') {
51 if (build_path.is_relative()) {
74 const std::string& preset) {
76 return absl::InvalidArgumentError(
"Preset name cannot be empty");
82 return absl::InvalidArgumentError(
83 absl::StrFormat(
"Invalid preset '%s'. Available presets: %s", preset,
84 absl::StrJoin(available,
", ")));
91 return absl::InternalError(
92 absl::StrFormat(
"Failed to create build directory: %s", ec.message()));
96 std::string command = absl::StrFormat(
"cmake --preset %s -B \"%s\"", preset,
100 command +=
" --debug-output";
104 command, absl::StrFormat(
"Configuring with preset '%s'", preset));
108 const std::string& target,
const std::string& config) {
112 return absl::FailedPreconditionError(absl::StrFormat(
113 "Build directory '%s' not configured. Run Configure first.",
118 std::string command =
121 if (!config.empty()) {
122 command += absl::StrFormat(
" --config %s", config);
125 if (!target.empty()) {
126 command += absl::StrFormat(
" --target %s", target);
130 command +=
" --parallel";
133 command +=
" --verbose";
138 absl::StrFormat(
"Building %s", target.empty() ?
"all targets" : target));
142 const std::string& filter,
const std::string& rom_path) {
146 return absl::FailedPreconditionError(absl::StrFormat(
147 "Build directory '%s' not configured. Run Configure first.",
152 std::string command = absl::StrFormat(
156 if (!filter.empty()) {
158 if (filter ==
"unit" || filter ==
"integration" || filter ==
"e2e" ||
159 filter ==
"stable" || filter ==
"experimental" ||
160 filter ==
"rom_dependent") {
161 command += absl::StrFormat(
" -L %s", filter);
164 command += absl::StrFormat(
" -R \"%s\"", filter);
169 std::string env_setup;
170 if (!rom_path.empty()) {
171 if (!fs::exists(rom_path)) {
172 return absl::NotFoundError(
173 absl::StrFormat(
"ROM file not found: %s", rom_path));
176 env_setup = absl::StrFormat(
177 "set YAZE_TEST_ROM_VANILLA=\"%s\" && set YAZE_TEST_ROM_PATH=\"%s\" && ",
180 env_setup = absl::StrFormat(
181 "YAZE_TEST_ROM_VANILLA=\"%s\" YAZE_TEST_ROM_PATH=\"%s\" ", rom_path,
187 command +=
" --parallel";
190 command +=
" --verbose";
193 std::string full_command = env_setup + command;
199 filter.empty() ?
"" : absl::StrFormat(
" (filter: %s)", filter)));
213 "Last operation: %s (exit code: %d, duration: %lds)",
228 result.
output =
"Build directory does not exist, nothing to clean";
230 result.
duration = std::chrono::seconds(0);
235 std::string command = absl::StrFormat(
"cmake --build \"%s\" --target clean",
245 return fs::exists(build_path) && fs::exists(build_path /
"CMakeCache.txt");
264 return absl::OkStatus();
272 const std::string& command,
const std::string& operation_name) {
276 return absl::UnavailableError(
"Another build operation is in progress");
281 auto start_time = std::chrono::steady_clock::now();
287 auto end_time = std::chrono::steady_clock::now();
289 std::chrono::duration_cast<std::chrono::seconds>(end_time - start_time);
292 auto& build_result = *result;
293 build_result.duration = duration;
294 build_result.command_executed = command;
311 const std::string& command,
const std::chrono::seconds& timeout) {
319 fs::path original_dir = fs::current_path();
323 fs::current_path(project_root, ec);
325 return absl::InternalError(
326 absl::StrFormat(
"Failed to change directory: %s", ec.message()));
332 std::string full_command = absl::StrFormat(
"cmd /c %s 2>&1", command);
333 FILE* pipe = _popen(full_command.c_str(),
"r");
336 std::string full_command = command +
" 2>&1";
337 FILE* pipe = popen(full_command.c_str(),
"r");
341 fs::current_path(original_dir, ec);
342 return absl::InternalError(
"Failed to execute command");
346 std::stringstream output_stream;
347 std::stringstream error_stream;
348 std::array<char, 4096> buffer;
349 size_t total_output = 0;
350 auto start_time = std::chrono::steady_clock::now();
354 int pipe_fd = fileno(pipe);
355 int flags = fcntl(pipe_fd, F_GETFL, 0);
356 fcntl(pipe_fd, F_SETFL, flags | O_NONBLOCK);
361 auto current_time = std::chrono::steady_clock::now();
362 auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
363 current_time - start_time);
365 if (elapsed >= timeout) {
372 fs::current_path(original_dir, ec);
373 return absl::DeadlineExceededError(absl::StrFormat(
374 "Command timed out after %ld seconds", timeout.count()));
378 if (fgets(buffer.data(), buffer.size(), pipe) !=
nullptr) {
379 size_t len = strlen(buffer.data());
385 output_stream << buffer.data();
388 std::string line(buffer.data());
389 std::transform(line.begin(), line.end(), line.begin(), ::tolower);
390 if (line.find(
"error") != std::string::npos ||
391 line.find(
"failed") != std::string::npos ||
392 line.find(
"fatal") != std::string::npos) {
393 error_stream << buffer.data();
403 if (errno == EAGAIN || errno == EWOULDBLOCK) {
404 std::this_thread::sleep_for(std::chrono::milliseconds(100));
417 int status = pclose(pipe);
418 if (WIFEXITED(status)) {
420 }
else if (WIFSIGNALED(status)) {
421 result.
exit_code = 128 + WTERMSIG(status);
428 fs::current_path(original_dir, ec);
432 result.
output = output_stream.str();
437 return absl::CancelledError(
"Operation was cancelled");
445 fs::path current = fs::current_path();
446 fs::path root = current;
449 while (!root.empty() && root != root.root_path()) {
451 if (fs::exists(root /
"CMakeLists.txt") &&
452 fs::exists(root /
"src" /
"yaze.cc") &&
453 fs::exists(root /
"CMakePresets.json")) {
454 return root.string();
457 if (fs::exists(root /
".git")) {
459 if (fs::exists(root /
"src" /
"cli") &&
460 fs::exists(root /
"src" /
"app")) {
461 return root.string();
464 root = root.parent_path();
468 return current.string();
474#elif defined(__APPLE__)
476#elif defined(__linux__)
484 std::vector<std::string> presets;
486 fs::path presets_file = fs::path(
GetProjectRoot()) /
"CMakePresets.json";
487 if (!fs::exists(presets_file)) {
492 std::ifstream file(presets_file);
497 std::stringstream buffer;
498 buffer << file.rdbuf();
499 std::string content = buffer.str();
506 bool in_configure_presets =
false;
507 bool in_preset_object =
false;
508 bool is_hidden =
false;
509 std::string current_preset_name;
512 std::regex configure_presets_regex(
"\"configurePresets\"\\s*:\\s*\\[");
513 std::regex preset_name_regex(
"\"name\"\\s*:\\s*\"([^\"]+)\"");
514 std::regex hidden_regex(
"\"hidden\"\\s*:\\s*true");
515 std::regex condition_regex(
"\"condition\"\\s*:");
517 std::istringstream stream(content);
521 while (std::getline(stream, line)) {
523 if (std::regex_search(line, configure_presets_regex)) {
524 in_configure_presets =
true;
528 if (!in_configure_presets)
532 for (
char c : line) {
535 if (brace_count == 1) {
536 in_preset_object =
true;
538 current_preset_name.clear();
540 }
else if (c ==
'}') {
542 if (brace_count == 0 && in_preset_object) {
544 if (!current_preset_name.empty() && !is_hidden) {
546 bool include =
false;
548 if (platform ==
"Windows") {
550 if (absl::StartsWith(current_preset_name,
"win-") ||
551 absl::StartsWith(current_preset_name,
"ci-windows") ||
552 (!absl::StartsWith(current_preset_name,
"mac-") &&
553 !absl::StartsWith(current_preset_name,
"lin-") &&
554 !absl::StartsWith(current_preset_name,
"ci-"))) {
557 }
else if (platform ==
"Darwin") {
559 if (absl::StartsWith(current_preset_name,
"mac-") ||
560 absl::StartsWith(current_preset_name,
"ci-macos") ||
561 (!absl::StartsWith(current_preset_name,
"win-") &&
562 !absl::StartsWith(current_preset_name,
"lin-") &&
563 !absl::StartsWith(current_preset_name,
"ci-"))) {
566 }
else if (platform ==
"Linux") {
568 if (absl::StartsWith(current_preset_name,
"lin-") ||
569 absl::StartsWith(current_preset_name,
"ci-linux") ||
570 (!absl::StartsWith(current_preset_name,
"win-") &&
571 !absl::StartsWith(current_preset_name,
"mac-") &&
572 !absl::StartsWith(current_preset_name,
"ci-"))) {
578 presets.push_back(current_preset_name);
581 in_preset_object =
false;
583 }
else if (c ==
']' && brace_count == -1) {
585 in_configure_presets =
false;
590 if (in_preset_object) {
593 if (std::regex_search(line, match, preset_name_regex)) {
594 current_preset_name = match[1].str();
598 if (std::regex_search(line, hidden_regex)) {
605 std::sort(presets.begin(), presets.end());
612 return std::find(available.begin(), available.end(), preset) !=
631 if (!parser.
GetString(
"preset").has_value()) {
632 return absl::InvalidArgumentError(
"--preset is required");
634 return absl::OkStatus();
642 std::string preset = parser.
GetString(
"preset").value();
643 std::string build_dir = parser.
GetString(
"build-dir").value_or(
"build_ai");
644 bool verbose = parser.
HasFlag(
"verbose");
657 return result.status();
662 formatter.
AddField(
"preset", preset);
664 formatter.
AddField(
"success", result->success ?
"true" :
"false");
665 formatter.
AddField(
"exit_code", std::to_string(result->exit_code));
667 absl::StrFormat(
"%ld seconds", result->duration.count()));
669 if (!result->output.empty()) {
671 const size_t max_lines = 100;
672 std::vector<std::string> lines = absl::StrSplit(result->output,
'\n');
673 if (lines.size() > max_lines) {
674 std::vector<std::string> truncated(lines.end() - max_lines, lines.end());
676 absl::StrFormat(
"[...truncated %zu lines...]\n%s",
677 lines.size() - max_lines,
678 absl::StrJoin(truncated,
"\n")));
680 formatter.
AddField(
"output", result->output);
684 if (!result->error_output.empty()) {
685 formatter.
AddField(
"errors", result->error_output);
690 if (!result->success) {
691 return absl::InternalError(
"Configuration failed");
694 return absl::OkStatus();
700 return absl::OkStatus();
708 std::string target = parser.
GetString(
"target").value_or(
"");
709 std::string config = parser.
GetString(
"config").value_or(
"");
710 std::string build_dir = parser.
GetString(
"build-dir").value_or(
"build_ai");
711 bool verbose = parser.
HasFlag(
"verbose");
719 build_tool_ = std::make_unique<BuildTool>(tool_config);
724 return result.status();
729 formatter.
AddField(
"target", target.empty() ?
"all" : target);
730 if (!config.empty()) {
731 formatter.
AddField(
"configuration", config);
733 formatter.
AddField(
"build_directory", build_dir);
734 formatter.
AddField(
"success", result->success ?
"true" :
"false");
735 formatter.
AddField(
"exit_code", std::to_string(result->exit_code));
737 absl::StrFormat(
"%ld seconds", result->duration.count()));
740 if (!result->output.empty()) {
741 const size_t max_lines = 100;
742 std::vector<std::string> lines = absl::StrSplit(result->output,
'\n');
743 if (lines.size() > max_lines) {
744 std::vector<std::string> truncated(lines.end() - max_lines, lines.end());
746 absl::StrFormat(
"[...truncated %zu lines...]\n%s",
747 lines.size() - max_lines,
748 absl::StrJoin(truncated,
"\n")));
749 formatter.
AddField(
"output_truncated",
"true");
751 formatter.
AddField(
"output", result->output);
755 if (!result->error_output.empty()) {
756 formatter.
AddField(
"errors", result->error_output);
761 if (!result->success) {
762 return absl::InternalError(
"Build failed");
765 return absl::OkStatus();
771 return absl::OkStatus();
779 std::string filter = parser.
GetString(
"filter").value_or(
"");
780 std::string rom_path = parser.
GetString(
"rom-path").value_or(
"");
781 std::string build_dir = parser.
GetString(
"build-dir").value_or(
"build_ai");
782 bool verbose = parser.
HasFlag(
"verbose");
789 config.
timeout = std::chrono::seconds(300);
794 auto result =
build_tool_->RunTests(filter, rom_path);
796 return result.status();
801 if (!filter.empty()) {
802 formatter.
AddField(
"filter", filter);
804 if (!rom_path.empty()) {
805 formatter.
AddField(
"rom_path", rom_path);
807 formatter.
AddField(
"build_directory", build_dir);
808 formatter.
AddField(
"success", result->success ?
"true" :
"false");
809 formatter.
AddField(
"exit_code", std::to_string(result->exit_code));
811 absl::StrFormat(
"%ld seconds", result->duration.count()));
814 if (!result->output.empty()) {
816 std::regex summary_regex(
817 R
"((\d+)% tests passed, (\d+) tests failed out of (\d+))");
819 if (std::regex_search(result->output, match, summary_regex)) {
820 formatter.
AddField(
"tests_total", match[3].str());
821 formatter.
AddField(
"tests_failed", match[2].str());
822 formatter.
AddField(
"pass_rate", match[1].str() +
"%");
826 const size_t max_lines = 50;
827 std::vector<std::string> lines = absl::StrSplit(result->output,
'\n');
828 if (lines.size() > max_lines) {
829 std::vector<std::string> truncated(lines.end() - max_lines, lines.end());
831 absl::StrFormat(
"[...truncated %zu lines...]\n%s",
832 lines.size() - max_lines,
833 absl::StrJoin(truncated,
"\n")));
835 formatter.
AddField(
"output", result->output);
839 if (!result->error_output.empty()) {
840 formatter.
AddField(
"errors", result->error_output);
845 if (!result->success) {
846 return absl::InternalError(
"Tests failed");
849 return absl::OkStatus();
855 return absl::OkStatus();
862 std::string build_dir = parser.
GetString(
"build-dir").value_or(
"build_ai");
874 formatter.
AddField(
"build_directory", build_dir);
875 formatter.
AddField(
"directory_ready",
876 build_tool_->IsBuildDirectoryReady() ?
"true" :
"false");
877 formatter.
AddField(
"operation_running", status.is_running ?
"true" :
"false");
879 if (status.is_running) {
880 formatter.
AddField(
"current_operation", status.current_operation);
883 auto now = std::chrono::system_clock::now();
884 auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
885 now - status.start_time);
887 absl::StrFormat(
"%ld seconds", elapsed.count()));
890 if (!status.last_result_summary.empty()) {
891 formatter.
AddField(
"last_result", status.last_result_summary);
895 auto presets =
build_tool_->ListAvailablePresets();
896 if (!presets.empty()) {
898 for (
const auto& preset : presets) {
906 if (last_result.has_value()) {
908 formatter.
AddField(
"command", last_result->command_executed);
909 formatter.
AddField(
"success", last_result->success ?
"true" :
"false");
910 formatter.
AddField(
"exit_code", std::to_string(last_result->exit_code));
913 absl::StrFormat(
"%ld seconds", last_result->duration.count()));
919 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.