@@ -455,6 +455,31 @@ namespace vix::commands::BuildCommand
455455 return o;
456456 }
457457 }
458+ else if (a == " --bin" )
459+ {
460+ o.exportBin = true ;
461+ }
462+ else if (a == " --out" )
463+ {
464+ auto v = util::take_value (args, i);
465+ if (!v)
466+ {
467+ error (" Missing value for --out <path>" );
468+ exitCode = 2 ;
469+ return o;
470+ }
471+ o.outPath = std::string (*v);
472+ }
473+ else if (a.rfind (" --out=" , 0 ) == 0 )
474+ {
475+ o.outPath = a.substr (std::string (" --out=" ).size ());
476+ if (o.outPath .empty ())
477+ {
478+ error (" Missing value for --out <path>" );
479+ exitCode = 2 ;
480+ return o;
481+ }
482+ }
458483 else if (a == " --clean" )
459484 {
460485 o.clean = true ;
@@ -892,6 +917,152 @@ namespace vix::commands::BuildCommand
892917 return false ;
893918 }
894919
920+ static std::string platform_executable_name (const std::string &name)
921+ {
922+ #ifdef _WIN32
923+ if (name.size () >= 4 && name.substr (name.size () - 4 ) == " .exe" )
924+ return name;
925+ return name + " .exe" ;
926+ #else
927+ return name;
928+ #endif
929+ }
930+
931+ static bool is_executable_candidate (const fs::path &p)
932+ {
933+ std::error_code ec{};
934+
935+ if (!fs::is_regular_file (p, ec) || ec)
936+ return false ;
937+
938+ #ifdef _WIN32
939+ return p.extension () == " .exe" ;
940+ #else
941+ const auto perms = fs::status (p, ec).permissions ();
942+ if (ec)
943+ return false ;
944+
945+ using pr = fs::perms;
946+ return (perms & pr::owner_exec) != pr::none ||
947+ (perms & pr::group_exec) != pr::none ||
948+ (perms & pr::others_exec) != pr::none;
949+ #endif
950+ }
951+
952+ static bool looks_like_test_binary (const fs::path &p)
953+ {
954+ const std::string n = p.filename ().string ();
955+ return n.find (" _test" ) != std::string::npos ||
956+ n.find (" _tests" ) != std::string::npos ||
957+ n.rfind (" test_" , 0 ) == 0 ;
958+ }
959+
960+ static std::optional<fs::path> resolve_main_executable (
961+ const fs::path &buildDir,
962+ const fs::path &projectDir,
963+ const std::string &buildTarget)
964+ {
965+ const std::string preferredBase =
966+ !buildTarget.empty ()
967+ ? buildTarget
968+ : projectDir.filename ().string ();
969+
970+ const std::string preferredName = platform_executable_name (preferredBase);
971+
972+ const std::vector<fs::path> preferredPaths = {
973+ buildDir / preferredName,
974+ buildDir / " bin" / preferredName,
975+ buildDir / " src" / preferredName};
976+
977+ for (const auto &p : preferredPaths)
978+ {
979+ if (is_executable_candidate (p) && !looks_like_test_binary (p))
980+ return p;
981+ }
982+
983+ std::vector<fs::path> candidates;
984+ std::error_code ec{};
985+
986+ for (auto it = fs::recursive_directory_iterator (
987+ buildDir,
988+ fs::directory_options::skip_permission_denied,
989+ ec);
990+ !ec && it != fs::recursive_directory_iterator ();
991+ ++it)
992+ {
993+ const fs::path p = it->path ();
994+
995+ if (p.string ().find (" CMakeFiles" ) != std::string::npos)
996+ continue ;
997+
998+ if (!is_executable_candidate (p))
999+ continue ;
1000+
1001+ if (looks_like_test_binary (p))
1002+ continue ;
1003+
1004+ #ifdef _WIN32
1005+ const std::string baseName = p.stem ().string ();
1006+ #else
1007+ const std::string baseName = p.filename ().string ();
1008+ #endif
1009+
1010+ if (baseName == preferredBase || p.filename ().string () == preferredName)
1011+ return p;
1012+
1013+ candidates.push_back (p);
1014+ }
1015+
1016+ if (candidates.size () == 1 )
1017+ return candidates.front ();
1018+
1019+ return std::nullopt ;
1020+ }
1021+
1022+ static bool export_built_binary (
1023+ const fs::path &sourceExe,
1024+ const fs::path &destination,
1025+ bool quiet)
1026+ {
1027+ std::error_code ec{};
1028+
1029+ fs::path finalDest = destination;
1030+
1031+ if (fs::exists (destination, ec) && fs::is_directory (destination, ec))
1032+ finalDest = destination / sourceExe.filename ();
1033+
1034+ const fs::path parent = finalDest.parent_path ();
1035+ if (!parent.empty ())
1036+ {
1037+ fs::create_directories (parent, ec);
1038+ if (ec)
1039+ {
1040+ error (" Failed to create output directory: " + parent.string ());
1041+ hint (ec.message ());
1042+ return false ;
1043+ }
1044+ }
1045+
1046+ fs::copy_file (sourceExe, finalDest, fs::copy_options::overwrite_existing, ec);
1047+ if (ec)
1048+ {
1049+ error (" Failed to export binary to: " + finalDest.string ());
1050+ hint (ec.message ());
1051+ return false ;
1052+ }
1053+
1054+ #ifndef _WIN32
1055+ const auto perms = fs::status (sourceExe, ec).permissions ();
1056+ if (!ec)
1057+ fs::permissions (finalDest, perms, fs::perm_options::replace, ec);
1058+ #endif
1059+
1060+ if (!quiet)
1061+ success (" Exported binary: " + finalDest.string ());
1062+
1063+ return true ;
1064+ }
1065+
8951066 class BuildCommand
8961067 {
8971068 public:
@@ -1226,6 +1397,37 @@ namespace vix::commands::BuildCommand
12261397 }
12271398 }
12281399
1400+ if (opt_.exportBin || !opt_.outPath .empty ())
1401+ {
1402+ const auto exeOpt = resolve_main_executable (
1403+ plan_.buildDir ,
1404+ plan_.projectDir ,
1405+ opt_.buildTarget );
1406+
1407+ if (!exeOpt)
1408+ {
1409+ error (" Unable to resolve the main executable to export." );
1410+ hint (" Use --build-target <name> if your project produces multiple executables." );
1411+ return 1 ;
1412+ }
1413+
1414+ fs::path dest;
1415+ if (opt_.exportBin )
1416+ {
1417+ dest = plan_.projectDir / exeOpt->filename ();
1418+ }
1419+ else
1420+ {
1421+ dest = fs::absolute (fs::path (opt_.outPath ));
1422+ }
1423+
1424+ if (!export_built_binary (*exeOpt, dest, opt_.quiet ))
1425+ return 1 ;
1426+ }
1427+
1428+ out.flush_to_stdout ();
1429+ return 0 ;
1430+
12291431 out.flush_to_stdout ();
12301432 return 0 ;
12311433 }
@@ -1234,13 +1436,21 @@ namespace vix::commands::BuildCommand
12341436 process::Options opt_;
12351437 process::Plan plan_{};
12361438 };
1439+
12371440 } // namespace
12381441
12391442 int run (const std::vector<std::string> &args)
12401443 {
12411444 int parseExit = 0 ;
12421445 process::Options opt = parse_args_or_exit (args, parseExit);
12431446
1447+ if (opt.exportBin && !opt.outPath .empty ())
1448+ {
1449+ error (" Options --bin and --out cannot be used together." );
1450+ hint (" Use either --bin or --out <path>." );
1451+ return 2 ;
1452+ }
1453+
12441454 if (parseExit == -2 )
12451455 return help ();
12461456 if (parseExit != 0 )
@@ -1279,6 +1489,8 @@ namespace vix::commands::BuildCommand
12791489 out << " release -> Ninja + Release (build-release)\n\n " ;
12801490
12811491 out << " Options:\n " ;
1492+ out << " --bin Export the built executable to the project root\n " ;
1493+ out << " --out <path> Export the built executable to a specific path\n " ;
12821494 out << " --preset <name> Preset to use (dev, dev-ninja, release)\n " ;
12831495 out << " --target <triple> Cross-compilation target triple (auto toolchain)\n " ;
12841496 out << " --sysroot <path> Sysroot for cross toolchain (optional)\n " ;
@@ -1319,6 +1531,8 @@ namespace vix::commands::BuildCommand
13191531 out << " vix build --target aarch64-linux-gnu\n " ;
13201532 out << " vix build --preset release --target aarch64-linux-gnu\n " ;
13211533 out << " vix build --linker lld -- -DVIX_SYNC_BUILD_TESTS=ON\n " ;
1534+ out << " vix build --bin\n " ;
1535+ out << " vix build --out dist/runner\n " ;
13221536 out << " VIX_BUILD_HEARTBEAT=1 vix build\n " ;
13231537 out << " vix build -j 8\n\n " ;
13241538
0 commit comments