Skip to content

Commit 2a7d503

Browse files
committed
feat(cli): add binary export support to vix build
1 parent e477b20 commit 2a7d503

2 files changed

Lines changed: 217 additions & 0 deletions

File tree

include/vix/cli/process/Process.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,9 @@ namespace vix::cli::process
154154

155155
bool withSqlite = false;
156156
bool withMySql = false;
157+
158+
bool exportBin = false;
159+
std::string outPath;
157160
};
158161

159162
/**

src/commands/BuildCommand.cpp

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)