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