yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
build_tool.cc
Go to the documentation of this file.
2
3#include <array>
4#include <cstdio>
5#include <filesystem>
6#include <fstream>
7#include <future>
8#include <memory>
9#include <regex>
10#include <sstream>
11
12#include "absl/strings/match.h"
13#include "absl/strings/str_cat.h"
14#include "absl/strings/str_format.h"
15#include "absl/strings/str_join.h"
16#include "absl/strings/str_split.h"
17
18#ifdef _WIN32
19#include <windows.h>
20#include <process.h>
21#else
22#include <fcntl.h>
23#include <signal.h>
24#include <sys/wait.h>
25#include <unistd.h>
26#endif
27
28namespace yaze {
29namespace cli {
30namespace agent {
31namespace tools {
32
33namespace fs = std::filesystem;
34
35// ============================================================================
36// BuildTool Implementation
37// ============================================================================
38
40 : config_(config) {
41 // Ensure build directory is set with a default value
42 if (config_.build_directory.empty()) {
43 config_.build_directory = "build_ai";
44 }
45
46 // Convert to absolute path if relative
47 fs::path build_path(config_.build_directory);
48 if (build_path.is_relative()) {
49 config_.build_directory = (fs::path(GetProjectRoot()) / build_path).string();
50 }
51}
52
54 // Cancel any running operation on destruction
55 if (is_running_) {
57 }
58
59 // Wait for any execution thread to complete
60 if (execution_thread_ && execution_thread_->joinable()) {
61 execution_thread_->join();
62 }
63}
64
65// ----------------------------------------------------------------------------
66// Public Methods
67// ----------------------------------------------------------------------------
68
69absl::StatusOr<BuildTool::BuildResult> BuildTool::Configure(
70 const std::string& preset) {
71 if (preset.empty()) {
72 return absl::InvalidArgumentError("Preset name cannot be empty");
73 }
74
75 // Validate preset exists
76 if (!IsPresetValid(preset)) {
77 auto available = ListAvailablePresets();
78 return absl::InvalidArgumentError(
79 absl::StrFormat("Invalid preset '%s'. Available presets: %s",
80 preset, absl::StrJoin(available, ", ")));
81 }
82
83 // Ensure build directory exists
84 std::error_code ec;
85 fs::create_directories(config_.build_directory, ec);
86 if (ec) {
87 return absl::InternalError(
88 absl::StrFormat("Failed to create build directory: %s", ec.message()));
89 }
90
91 // Build cmake command
92 std::string command = absl::StrFormat(
93 "cmake --preset %s -B \"%s\"",
94 preset, config_.build_directory);
95
96 if (config_.verbose) {
97 command += " --debug-output";
98 }
99
100 return ExecuteCommand(command,
101 absl::StrFormat("Configuring with preset '%s'", preset));
102}
103
104absl::StatusOr<BuildTool::BuildResult> BuildTool::Build(
105 const std::string& target, const std::string& config) {
106
107 // Check if build directory exists
108 if (!IsBuildDirectoryReady()) {
109 return absl::FailedPreconditionError(
110 absl::StrFormat("Build directory '%s' not configured. Run Configure first.",
112 }
113
114 // Build cmake command
115 std::string command = absl::StrFormat(
116 "cmake --build \"%s\"", config_.build_directory);
117
118 if (!config.empty()) {
119 command += absl::StrFormat(" --config %s", config);
120 }
121
122 if (!target.empty()) {
123 command += absl::StrFormat(" --target %s", target);
124 }
125
126 // Add parallel jobs based on CPU count
127 command += " --parallel";
128
129 if (config_.verbose) {
130 command += " --verbose";
131 }
132
133 return ExecuteCommand(command,
134 absl::StrFormat("Building %s",
135 target.empty() ? "all targets" : target));
136}
137
138absl::StatusOr<BuildTool::BuildResult> BuildTool::RunTests(
139 const std::string& filter, const std::string& rom_path) {
140
141 // Check if build directory exists
142 if (!IsBuildDirectoryReady()) {
143 return absl::FailedPreconditionError(
144 absl::StrFormat("Build directory '%s' not configured. Run Configure first.",
146 }
147
148 // Build ctest command
149 std::string command = absl::StrFormat(
150 "ctest --test-dir \"%s\" --output-on-failure",
152
153 // Add filter if specified
154 if (!filter.empty()) {
155 // Check if filter is a label (unit, integration, etc.) or a pattern
156 if (filter == "unit" || filter == "integration" || filter == "e2e" ||
157 filter == "stable" || filter == "experimental" ||
158 filter == "rom_dependent") {
159 command += absl::StrFormat(" -L %s", filter);
160 } else {
161 // Treat as regex pattern
162 command += absl::StrFormat(" -R \"%s\"", filter);
163 }
164 }
165
166 // Add ROM path environment variable if specified
167 std::string env_setup;
168 if (!rom_path.empty()) {
169 if (!fs::exists(rom_path)) {
170 return absl::NotFoundError(
171 absl::StrFormat("ROM file not found: %s", rom_path));
172 }
173#ifdef _WIN32
174 env_setup = absl::StrFormat("set YAZE_TEST_ROM_PATH=\"%s\" && ", rom_path);
175#else
176 env_setup = absl::StrFormat("YAZE_TEST_ROM_PATH=\"%s\" ", rom_path);
177#endif
178 }
179
180 // Add parallel test execution
181 command += " --parallel";
182
183 if (config_.verbose) {
184 command += " --verbose";
185 }
186
187 std::string full_command = env_setup + command;
188
189 return ExecuteCommand(full_command,
190 absl::StrFormat("Running tests%s",
191 filter.empty() ? "" : absl::StrFormat(" (filter: %s)", filter)));
192}
193
195 std::lock_guard<std::mutex> lock(status_mutex_);
196
197 BuildStatus status;
198 status.is_running = is_running_;
201 status.progress_percent = -1; // Unknown
202
203 if (last_result_.has_value()) {
204 status.last_result_summary = absl::StrFormat(
205 "Last operation: %s (exit code: %d, duration: %lds)",
206 last_result_->success ? "SUCCESS" : "FAILED",
207 last_result_->exit_code,
208 last_result_->duration.count());
209 } else {
210 status.last_result_summary = "No operations executed yet";
211 }
212
213 return status;
214}
215
216absl::StatusOr<BuildTool::BuildResult> BuildTool::Clean() {
217 if (!IsBuildDirectoryReady()) {
218 // Build directory doesn't exist, nothing to clean
219 BuildResult result;
220 result.success = true;
221 result.output = "Build directory does not exist, nothing to clean";
222 result.exit_code = 0;
223 result.duration = std::chrono::seconds(0);
224 result.command_executed = "Clean";
225 return result;
226 }
227
228 std::string command = absl::StrFormat(
229 "cmake --build \"%s\" --target clean",
231
232 return ExecuteCommand(command, "Cleaning build directory");
233}
234
236 fs::path build_path(config_.build_directory);
237
238 // Check if directory exists and contains CMakeCache.txt
239 return fs::exists(build_path) &&
240 fs::exists(build_path / "CMakeCache.txt");
241}
242
243std::vector<std::string> BuildTool::ListAvailablePresets() const {
244 return ParsePresetsFile();
245}
246
247std::optional<BuildTool::BuildResult> BuildTool::GetLastResult() const {
248 std::lock_guard<std::mutex> lock(status_mutex_);
249 return last_result_;
250}
251
253 cancel_requested_ = true;
254
255 if (execution_thread_ && execution_thread_->joinable()) {
256 execution_thread_->join();
257 }
258
259 return absl::OkStatus();
260}
261
262// ----------------------------------------------------------------------------
263// Private Methods
264// ----------------------------------------------------------------------------
265
266absl::StatusOr<BuildTool::BuildResult> BuildTool::ExecuteCommand(
267 const std::string& command, const std::string& operation_name) {
268
269 // Check if another operation is running
270 if (is_running_.exchange(true)) {
271 return absl::UnavailableError("Another build operation is in progress");
272 }
273
274 // Update status
275 UpdateStatus(operation_name, true);
276 auto start_time = std::chrono::steady_clock::now();
277
278 // Execute command
279 auto result = ExecuteCommandInternal(command, config_.timeout);
280
281 // Calculate duration
282 auto end_time = std::chrono::steady_clock::now();
283 auto duration = std::chrono::duration_cast<std::chrono::seconds>(
284 end_time - start_time);
285
286 if (result.ok()) {
287 auto& build_result = *result;
288 build_result.duration = duration;
289 build_result.command_executed = command;
290
291 // Store last result
292 {
293 std::lock_guard<std::mutex> lock(status_mutex_);
294 last_result_ = build_result;
295 }
296 }
297
298 // Update status
299 UpdateStatus("", false);
300 is_running_ = false;
301
302 return result;
303}
304
305absl::StatusOr<BuildTool::BuildResult> BuildTool::ExecuteCommandInternal(
306 const std::string& command, const std::chrono::seconds& timeout) {
307
308 BuildResult result;
309 result.command_executed = command;
310 result.success = false;
311 result.exit_code = -1;
312
313 // Change to project root before executing
314 fs::path original_dir = fs::current_path();
315 fs::path project_root(GetProjectRoot());
316
317 std::error_code ec;
318 fs::current_path(project_root, ec);
319 if (ec) {
320 return absl::InternalError(
321 absl::StrFormat("Failed to change directory: %s", ec.message()));
322 }
323
324 // Platform-specific command execution
325#ifdef _WIN32
326 // Windows implementation using _popen
327 std::string full_command = absl::StrFormat("cmd /c %s 2>&1", command);
328 FILE* pipe = _popen(full_command.c_str(), "r");
329#else
330 // Unix implementation using popen
331 std::string full_command = command + " 2>&1";
332 FILE* pipe = popen(full_command.c_str(), "r");
333#endif
334
335 if (!pipe) {
336 fs::current_path(original_dir, ec);
337 return absl::InternalError("Failed to execute command");
338 }
339
340 // Read output with timeout protection
341 std::stringstream output_stream;
342 std::stringstream error_stream;
343 std::array<char, 4096> buffer;
344 size_t total_output = 0;
345 auto start_time = std::chrono::steady_clock::now();
346
347 // Set non-blocking mode for better timeout handling (Unix only)
348#ifndef _WIN32
349 int pipe_fd = fileno(pipe);
350 int flags = fcntl(pipe_fd, F_GETFL, 0);
351 fcntl(pipe_fd, F_SETFL, flags | O_NONBLOCK);
352#endif
353
354 while (!cancel_requested_) {
355 // Check timeout
356 auto current_time = std::chrono::steady_clock::now();
357 auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
358 current_time - start_time);
359
360 if (elapsed >= timeout) {
361 // Timeout reached
362#ifdef _WIN32
363 _pclose(pipe);
364#else
365 pclose(pipe);
366#endif
367 fs::current_path(original_dir, ec);
368 return absl::DeadlineExceededError(
369 absl::StrFormat("Command timed out after %ld seconds",
370 timeout.count()));
371 }
372
373 // Read from pipe
374 if (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
375 size_t len = strlen(buffer.data());
376 total_output += len;
377
378 // Check output size limit
380 total_output <= static_cast<size_t>(config_.max_output_size)) {
381 output_stream << buffer.data();
382
383 // Try to separate errors (lines containing "error", "warning", "failed")
384 std::string line(buffer.data());
385 std::transform(line.begin(), line.end(), line.begin(), ::tolower);
386 if (line.find("error") != std::string::npos ||
387 line.find("failed") != std::string::npos ||
388 line.find("fatal") != std::string::npos) {
389 error_stream << buffer.data();
390 }
391 }
392 } else {
393 // Check if it's EOF or just no data available
394 if (feof(pipe)) {
395 break; // End of stream
396 }
397 // On Unix, sleep briefly if no data available
398#ifndef _WIN32
399 if (errno == EAGAIN || errno == EWOULDBLOCK) {
400 std::this_thread::sleep_for(std::chrono::milliseconds(100));
401 clearerr(pipe);
402 continue;
403 }
404#endif
405 break; // Error or EOF
406 }
407 }
408
409 // Get exit code
410#ifdef _WIN32
411 result.exit_code = _pclose(pipe);
412#else
413 int status = pclose(pipe);
414 if (WIFEXITED(status)) {
415 result.exit_code = WEXITSTATUS(status);
416 } else if (WIFSIGNALED(status)) {
417 result.exit_code = 128 + WTERMSIG(status);
418 } else {
419 result.exit_code = -1;
420 }
421#endif
422
423 // Restore original directory
424 fs::current_path(original_dir, ec);
425
426 // Set results
427 result.success = (result.exit_code == 0);
428 result.output = output_stream.str();
429 result.error_output = error_stream.str();
430
431 // If we were cancelled, return cancelled error
432 if (cancel_requested_) {
433 return absl::CancelledError("Operation was cancelled");
434 }
435
436 return result;
437}
438
439std::string BuildTool::GetProjectRoot() const {
440 // Look for common project markers to find the root
441 fs::path current = fs::current_path();
442 fs::path root = current;
443
444 // Walk up the directory tree looking for project markers
445 while (!root.empty() && root != root.root_path()) {
446 // Check for yaze-specific markers
447 if (fs::exists(root / "CMakeLists.txt") &&
448 fs::exists(root / "src" / "yaze.cc") &&
449 fs::exists(root / "CMakePresets.json")) {
450 return root.string();
451 }
452 // Also check for .git directory as a fallback
453 if (fs::exists(root / ".git")) {
454 // Verify this is the yaze project
455 if (fs::exists(root / "src" / "cli") &&
456 fs::exists(root / "src" / "app")) {
457 return root.string();
458 }
459 }
460 root = root.parent_path();
461 }
462
463 // Default to current directory if project root not found
464 return current.string();
465}
466
467std::string BuildTool::GetCurrentPlatform() const {
468#ifdef _WIN32
469 return "Windows";
470#elif defined(__APPLE__)
471 return "Darwin";
472#elif defined(__linux__)
473 return "Linux";
474#else
475 return "Unknown";
476#endif
477}
478
479std::vector<std::string> BuildTool::ParsePresetsFile() const {
480 std::vector<std::string> presets;
481
482 fs::path presets_file = fs::path(GetProjectRoot()) / "CMakePresets.json";
483 if (!fs::exists(presets_file)) {
484 return presets;
485 }
486
487 // Read the file
488 std::ifstream file(presets_file);
489 if (!file) {
490 return presets;
491 }
492
493 std::stringstream buffer;
494 buffer << file.rdbuf();
495 std::string content = buffer.str();
496
497 // Get current platform for filtering
498 std::string platform = GetCurrentPlatform();
499
500 // Parse JSON to find configure presets
501 // We need to track whether we're in configurePresets section
502 bool in_configure_presets = false;
503 bool in_preset_object = false;
504 bool is_hidden = false;
505 std::string current_preset_name;
506
507 // Simple state machine for JSON parsing
508 std::regex configure_presets_regex("\"configurePresets\"\\s*:\\s*\\[");
509 std::regex preset_name_regex("\"name\"\\s*:\\s*\"([^\"]+)\"");
510 std::regex hidden_regex("\"hidden\"\\s*:\\s*true");
511 std::regex condition_regex("\"condition\"\\s*:");
512
513 std::istringstream stream(content);
514 std::string line;
515 int brace_count = 0;
516
517 while (std::getline(stream, line)) {
518 // Check if entering configurePresets
519 if (std::regex_search(line, configure_presets_regex)) {
520 in_configure_presets = true;
521 continue;
522 }
523
524 if (!in_configure_presets) continue;
525
526 // Track braces to know when we're in a preset object
527 for (char c : line) {
528 if (c == '{') {
529 brace_count++;
530 if (brace_count == 1) {
531 in_preset_object = true;
532 is_hidden = false;
533 current_preset_name.clear();
534 }
535 } else if (c == '}') {
536 brace_count--;
537 if (brace_count == 0 && in_preset_object) {
538 // End of preset object, add if valid
539 if (!current_preset_name.empty() && !is_hidden) {
540 // Filter by platform
541 bool include = false;
542
543 if (platform == "Windows") {
544 // Include Windows presets and generic ones
545 if (absl::StartsWith(current_preset_name, "win-") ||
546 absl::StartsWith(current_preset_name, "ci-windows") ||
547 (!absl::StartsWith(current_preset_name, "mac-") &&
548 !absl::StartsWith(current_preset_name, "lin-") &&
549 !absl::StartsWith(current_preset_name, "ci-"))) {
550 include = true;
551 }
552 } else if (platform == "Darwin") {
553 // Include macOS presets and generic ones
554 if (absl::StartsWith(current_preset_name, "mac-") ||
555 absl::StartsWith(current_preset_name, "ci-macos") ||
556 (!absl::StartsWith(current_preset_name, "win-") &&
557 !absl::StartsWith(current_preset_name, "lin-") &&
558 !absl::StartsWith(current_preset_name, "ci-"))) {
559 include = true;
560 }
561 } else if (platform == "Linux") {
562 // Include Linux presets and generic ones
563 if (absl::StartsWith(current_preset_name, "lin-") ||
564 absl::StartsWith(current_preset_name, "ci-linux") ||
565 (!absl::StartsWith(current_preset_name, "win-") &&
566 !absl::StartsWith(current_preset_name, "mac-") &&
567 !absl::StartsWith(current_preset_name, "ci-"))) {
568 include = true;
569 }
570 }
571
572 if (include) {
573 presets.push_back(current_preset_name);
574 }
575 }
576 in_preset_object = false;
577 }
578 } else if (c == ']' && brace_count == -1) {
579 // End of configurePresets array
580 in_configure_presets = false;
581 break;
582 }
583 }
584
585 if (in_preset_object) {
586 // Look for preset name
587 std::smatch match;
588 if (std::regex_search(line, match, preset_name_regex)) {
589 current_preset_name = match[1].str();
590 }
591
592 // Check if hidden
593 if (std::regex_search(line, hidden_regex)) {
594 is_hidden = true;
595 }
596 }
597 }
598
599 // Sort presets alphabetically
600 std::sort(presets.begin(), presets.end());
601
602 return presets;
603}
604
605bool BuildTool::IsPresetValid(const std::string& preset) const {
606 auto available = ListAvailablePresets();
607 return std::find(available.begin(), available.end(), preset) != available.end();
608}
609
610void BuildTool::UpdateStatus(const std::string& operation, bool is_running) {
611 std::lock_guard<std::mutex> lock(status_mutex_);
612 current_operation_ = operation;
613 is_running_ = is_running;
614 if (is_running) {
615 operation_start_time_ = std::chrono::system_clock::now();
616 }
617}
618
619// ============================================================================
620// Command Handler Implementations
621// ============================================================================
622
624 const resources::ArgumentParser& parser) {
625 if (!parser.GetString("preset").has_value()) {
626 return absl::InvalidArgumentError("--preset is required");
627 }
628 return absl::OkStatus();
629}
630
632 Rom* rom, const resources::ArgumentParser& parser,
633 resources::OutputFormatter& formatter) {
634
635 // Get parameters
636 std::string preset = parser.GetString("preset").value();
637 std::string build_dir = parser.GetString("build-dir").value_or("build_ai");
638 bool verbose = parser.HasFlag("verbose");
639
640 // Create build tool with config
642 config.build_directory = build_dir;
643 config.verbose = verbose;
644 config.capture_output = true;
645
646 build_tool_ = std::make_unique<BuildTool>(config);
647
648 // Execute configuration
649 auto result = build_tool_->Configure(preset);
650 if (!result.ok()) {
651 return result.status();
652 }
653
654 // Format output
655 formatter.BeginObject("Build Configuration");
656 formatter.AddField("preset", preset);
657 formatter.AddField("build_directory", config.build_directory);
658 formatter.AddField("success", result->success ? "true" : "false");
659 formatter.AddField("exit_code", std::to_string(result->exit_code));
660 formatter.AddField("duration",
661 absl::StrFormat("%ld seconds", result->duration.count()));
662
663 if (!result->output.empty()) {
664 // Truncate output if too long
665 const size_t max_lines = 100;
666 std::vector<std::string> lines = absl::StrSplit(result->output, '\n');
667 if (lines.size() > max_lines) {
668 std::vector<std::string> truncated(
669 lines.end() - max_lines, lines.end());
670 formatter.AddField("output",
671 absl::StrFormat("[...truncated %zu lines...]\n%s",
672 lines.size() - max_lines,
673 absl::StrJoin(truncated, "\n")));
674 } else {
675 formatter.AddField("output", result->output);
676 }
677 }
678
679 if (!result->error_output.empty()) {
680 formatter.AddField("errors", result->error_output);
681 }
682
683 formatter.EndObject();
684
685 if (!result->success) {
686 return absl::InternalError("Configuration failed");
687 }
688
689 return absl::OkStatus();
690}
691
693 const resources::ArgumentParser& parser) {
694 // All arguments are optional
695 return absl::OkStatus();
696}
697
699 Rom* rom, const resources::ArgumentParser& parser,
700 resources::OutputFormatter& formatter) {
701
702 // Get parameters
703 std::string target = parser.GetString("target").value_or("");
704 std::string config = parser.GetString("config").value_or("");
705 std::string build_dir = parser.GetString("build-dir").value_or("build_ai");
706 bool verbose = parser.HasFlag("verbose");
707
708 // Create build tool
709 BuildTool::BuildConfig tool_config;
710 tool_config.build_directory = build_dir;
711 tool_config.verbose = verbose;
712 tool_config.capture_output = true;
713
714 build_tool_ = std::make_unique<BuildTool>(tool_config);
715
716 // Execute build
717 auto result = build_tool_->Build(target, config);
718 if (!result.ok()) {
719 return result.status();
720 }
721
722 // Format output
723 formatter.BeginObject("Build Compilation");
724 formatter.AddField("target", target.empty() ? "all" : target);
725 if (!config.empty()) {
726 formatter.AddField("configuration", config);
727 }
728 formatter.AddField("build_directory", build_dir);
729 formatter.AddField("success", result->success ? "true" : "false");
730 formatter.AddField("exit_code", std::to_string(result->exit_code));
731 formatter.AddField("duration",
732 absl::StrFormat("%ld seconds", result->duration.count()));
733
734 // Limit output size for readability
735 if (!result->output.empty()) {
736 const size_t max_lines = 100;
737 std::vector<std::string> lines = absl::StrSplit(result->output, '\n');
738 if (lines.size() > max_lines) {
739 std::vector<std::string> truncated(
740 lines.end() - max_lines, lines.end());
741 formatter.AddField("output",
742 absl::StrFormat("[...truncated %zu lines...]\n%s",
743 lines.size() - max_lines,
744 absl::StrJoin(truncated, "\n")));
745 formatter.AddField("output_truncated", "true");
746 } else {
747 formatter.AddField("output", result->output);
748 }
749 }
750
751 if (!result->error_output.empty()) {
752 formatter.AddField("errors", result->error_output);
753 }
754
755 formatter.EndObject();
756
757 if (!result->success) {
758 return absl::InternalError("Build failed");
759 }
760
761 return absl::OkStatus();
762}
763
765 const resources::ArgumentParser& parser) {
766 // All arguments are optional
767 return absl::OkStatus();
768}
769
771 Rom* rom, const resources::ArgumentParser& parser,
772 resources::OutputFormatter& formatter) {
773
774 // Get parameters
775 std::string filter = parser.GetString("filter").value_or("");
776 std::string rom_path = parser.GetString("rom-path").value_or("");
777 std::string build_dir = parser.GetString("build-dir").value_or("build_ai");
778 bool verbose = parser.HasFlag("verbose");
779
780 // Create build tool
782 config.build_directory = build_dir;
783 config.verbose = verbose;
784 config.capture_output = true;
785 config.timeout = std::chrono::seconds(300); // 5 minutes for tests
786
787 build_tool_ = std::make_unique<BuildTool>(config);
788
789 // Execute tests
790 auto result = build_tool_->RunTests(filter, rom_path);
791 if (!result.ok()) {
792 return result.status();
793 }
794
795 // Format output
796 formatter.BeginObject("Test Execution");
797 if (!filter.empty()) {
798 formatter.AddField("filter", filter);
799 }
800 if (!rom_path.empty()) {
801 formatter.AddField("rom_path", rom_path);
802 }
803 formatter.AddField("build_directory", build_dir);
804 formatter.AddField("success", result->success ? "true" : "false");
805 formatter.AddField("exit_code", std::to_string(result->exit_code));
806 formatter.AddField("duration",
807 absl::StrFormat("%ld seconds", result->duration.count()));
808
809 // Parse test results from output
810 if (!result->output.empty()) {
811 // Look for test summary
812 std::regex summary_regex(
813 R"((\d+)% tests passed, (\d+) tests failed out of (\d+))");
814 std::smatch match;
815 if (std::regex_search(result->output, match, summary_regex)) {
816 formatter.AddField("tests_total", match[3].str());
817 formatter.AddField("tests_failed", match[2].str());
818 formatter.AddField("pass_rate", match[1].str() + "%");
819 }
820
821 // Include last part of output for visibility
822 const size_t max_lines = 50;
823 std::vector<std::string> lines = absl::StrSplit(result->output, '\n');
824 if (lines.size() > max_lines) {
825 std::vector<std::string> truncated(
826 lines.end() - max_lines, lines.end());
827 formatter.AddField("output",
828 absl::StrFormat("[...truncated %zu lines...]\n%s",
829 lines.size() - max_lines,
830 absl::StrJoin(truncated, "\n")));
831 } else {
832 formatter.AddField("output", result->output);
833 }
834 }
835
836 if (!result->error_output.empty()) {
837 formatter.AddField("errors", result->error_output);
838 }
839
840 formatter.EndObject();
841
842 if (!result->success) {
843 return absl::InternalError("Tests failed");
844 }
845
846 return absl::OkStatus();
847}
848
850 const resources::ArgumentParser& parser) {
851 // All arguments are optional
852 return absl::OkStatus();
853}
854
856 Rom* rom, const resources::ArgumentParser& parser,
857 resources::OutputFormatter& formatter) {
858
859 std::string build_dir = parser.GetString("build-dir").value_or("build_ai");
860
861 // Create build tool
863 config.build_directory = build_dir;
864
865 build_tool_ = std::make_unique<BuildTool>(config);
866
867 // Get status
868 auto status = build_tool_->GetBuildStatus();
869
870 formatter.BeginObject("Build Status");
871 formatter.AddField("build_directory", build_dir);
872 formatter.AddField("directory_ready",
873 build_tool_->IsBuildDirectoryReady() ? "true" : "false");
874 formatter.AddField("operation_running",
875 status.is_running ? "true" : "false");
876
877 if (status.is_running) {
878 formatter.AddField("current_operation", status.current_operation);
879
880 // Calculate elapsed time
881 auto now = std::chrono::system_clock::now();
882 auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
883 now - status.start_time);
884 formatter.AddField("elapsed_time",
885 absl::StrFormat("%ld seconds", elapsed.count()));
886 }
887
888 if (!status.last_result_summary.empty()) {
889 formatter.AddField("last_result", status.last_result_summary);
890 }
891
892 // List available presets
893 auto presets = build_tool_->ListAvailablePresets();
894 if (!presets.empty()) {
895 formatter.BeginArray("available_presets");
896 for (const auto& preset : presets) {
897 formatter.AddArrayItem(preset);
898 }
899 formatter.EndArray();
900 }
901
902 // Check for last build result
903 auto last_result = build_tool_->GetLastResult();
904 if (last_result.has_value()) {
905 formatter.BeginObject("last_build");
906 formatter.AddField("command", last_result->command_executed);
907 formatter.AddField("success", last_result->success ? "true" : "false");
908 formatter.AddField("exit_code", std::to_string(last_result->exit_code));
909 formatter.AddField("duration",
910 absl::StrFormat("%ld seconds", last_result->duration.count()));
911 formatter.EndObject();
912 }
913
914 formatter.EndObject();
915
916 return absl::OkStatus();
917}
918
919} // namespace tools
920} // namespace agent
921} // namespace cli
922} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:24
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
std::chrono::system_clock::time_point operation_start_time_
Definition build_tool.h:160
std::optional< BuildResult > GetLastResult() const
Get the last build result.
absl::StatusOr< BuildResult > Configure(const std::string &preset)
Configure the build system with a CMake preset.
Definition build_tool.cc:69
std::optional< BuildResult > last_result_
Definition build_tool.h:159
bool IsBuildDirectoryReady() const
Check if a build directory exists and is configured.
absl::StatusOr< BuildResult > ExecuteCommandInternal(const std::string &command, const std::chrono::seconds &timeout)
BuildStatus GetBuildStatus() const
Get current build status.
std::vector< std::string > ParsePresetsFile() const
std::string GetProjectRoot() const
std::string GetCurrentPlatform() const
absl::StatusOr< BuildResult > Clean()
Clean the build directory.
std::unique_ptr< std::thread > execution_thread_
Definition build_tool.h:161
std::atomic< bool > cancel_requested_
Definition build_tool.h:162
std::atomic< bool > is_running_
Definition build_tool.h:157
absl::StatusOr< BuildResult > ExecuteCommand(const std::string &command, const std::string &operation_name)
absl::Status CancelCurrentOperation()
Cancel the current build operation.
absl::StatusOr< BuildResult > Build(const std::string &target="", const std::string &config="")
Build a specific target or all targets.
bool IsPresetValid(const std::string &preset) const
void UpdateStatus(const std::string &operation, bool is_running)
std::vector< std::string > ListAvailablePresets() const
List available CMake presets.
absl::StatusOr< BuildResult > RunTests(const std::string &filter="", const std::string &rom_path="")
Run tests with optional filter.
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.
Utility for consistent output formatting across commands.
void BeginArray(const std::string &key)
Begin an array.
void AddArrayItem(const std::string &item)
Add an item to current array.
void BeginObject(const std::string &title="")
Start a JSON object or text section.
void EndObject()
End a JSON object or text section.
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
std::chrono::system_clock::time_point start_time
Definition build_tool.h:49