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