Skip to content

Commit 77d4652

Browse files
committed
feat(cli): resolve dev session executable robustly
1 parent 4629367 commit 77d4652

2 files changed

Lines changed: 113 additions & 9 deletions

File tree

include/vix/cli/commands/run/dev/DevSession.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include <map>
2323
#include <string>
2424
#include <vector>
25+
#include <optional>
2526

2627
#include <vix/cli/commands/run/RunDetail.hpp>
2728
#include <vix/cli/commands/run/dev/DevChangeClassifier.hpp>
@@ -98,7 +99,7 @@ namespace vix::commands::RunCommand::dev
9899

99100
int rebuild_for_change(DevChangeKind kind) const;
100101

101-
fs::path executable_path() const;
102+
std::optional<fs::path> executable_path() const;
102103

103104
#ifndef _WIN32
104105
int run_child_once(const fs::path &exePath);

src/commands/run/dev/DevSession.cpp

Lines changed: 111 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
#include <system_error>
2626
#include <thread>
2727
#include <utility>
28+
#include <optional>
29+
#include <algorithm>
2830

2931
#ifndef _WIN32
3032
#include <signal.h>
@@ -124,19 +126,20 @@ namespace vix::commands::RunCommand::dev
124126
continue;
125127
}
126128

127-
const fs::path exePath = executable_path();
129+
const std::optional<fs::path> exePath = executable_path();
128130

129-
if (!fs::exists(exePath))
131+
if (!exePath)
130132
{
131133
result.exitCode = 1;
132-
result.message = "Dev executable not found: " + exePath.string();
134+
result.message = "Dev executable not found for target: " + options_.targetName;
133135

134136
error(result.message);
135-
hint("Make sure your CMakeLists.txt defines an executable named '" + options_.targetName + "'.");
137+
hint("Build directory: " + options_.buildDir.string());
138+
hint("Make sure your CMakeLists.txt defines an executable target named '" + options_.targetName + "'.");
136139
return result;
137140
}
138141

139-
const int runCode = run_child_once(exePath);
142+
const int runCode = run_child_once(*exePath);
140143

141144
if (runCode != 0)
142145
{
@@ -308,11 +311,111 @@ namespace vix::commands::RunCommand::dev
308311
return 0;
309312
}
310313

311-
fs::path DevSession::executable_path() const
314+
std::optional<fs::path> DevSession::executable_path() const
312315
{
313-
return options_.buildDir / executable_name(options_.targetName);
314-
}
316+
const std::string exeName = executable_name(options_.targetName);
317+
318+
auto is_executable_candidate = [](const fs::path &path) -> bool
319+
{
320+
std::error_code ec;
321+
322+
if (!fs::is_regular_file(path, ec) || ec)
323+
return false;
324+
325+
#ifdef _WIN32
326+
return path.extension() == ".exe";
327+
#else
328+
const auto perms = fs::status(path, ec).permissions();
329+
if (ec)
330+
return false;
331+
332+
using pr = fs::perms;
333+
334+
return (perms & pr::owner_exec) != pr::none ||
335+
(perms & pr::group_exec) != pr::none ||
336+
(perms & pr::others_exec) != pr::none;
337+
#endif
338+
};
339+
340+
auto looks_like_test_binary = [](const fs::path &path) -> bool
341+
{
342+
const std::string name = path.filename().string();
343+
344+
return name.find("_test") != std::string::npos ||
345+
name.find("_tests") != std::string::npos ||
346+
name.rfind("test_", 0) == 0;
347+
};
315348

349+
const std::vector<fs::path> preferred = {
350+
options_.buildDir / exeName,
351+
options_.buildDir / "bin" / exeName,
352+
options_.buildDir / "src" / exeName};
353+
354+
for (const auto &path : preferred)
355+
{
356+
if (is_executable_candidate(path))
357+
return path;
358+
}
359+
360+
std::vector<fs::path> exactNameCandidates;
361+
std::vector<fs::path> otherCandidates;
362+
363+
std::error_code ec;
364+
for (auto it = fs::recursive_directory_iterator(
365+
options_.buildDir,
366+
fs::directory_options::skip_permission_denied,
367+
ec);
368+
!ec && it != fs::recursive_directory_iterator();
369+
++it)
370+
{
371+
const fs::path path = it->path();
372+
373+
if (path.string().find("CMakeFiles") != std::string::npos)
374+
continue;
375+
376+
if (!is_executable_candidate(path))
377+
continue;
378+
379+
if (looks_like_test_binary(path))
380+
continue;
381+
382+
#ifdef _WIN32
383+
const std::string baseName = path.stem().string();
384+
#else
385+
const std::string baseName = path.filename().string();
386+
#endif
387+
388+
if (baseName == options_.targetName)
389+
exactNameCandidates.push_back(path);
390+
else
391+
otherCandidates.push_back(path);
392+
}
393+
394+
auto prefer_bin_path = [](const fs::path &a, const fs::path &b) -> bool
395+
{
396+
const bool aBin = a.string().find("/bin/") != std::string::npos ||
397+
a.string().find("\\bin\\") != std::string::npos;
398+
399+
const bool bBin = b.string().find("/bin/") != std::string::npos ||
400+
b.string().find("\\bin\\") != std::string::npos;
401+
402+
if (aBin != bBin)
403+
return aBin;
404+
405+
return a.string().size() < b.string().size();
406+
};
407+
408+
if (!exactNameCandidates.empty())
409+
{
410+
std::sort(exactNameCandidates.begin(), exactNameCandidates.end(), prefer_bin_path);
411+
return exactNameCandidates.front();
412+
}
413+
414+
if (otherCandidates.size() == 1)
415+
return otherCandidates.front();
416+
417+
return std::nullopt;
418+
}
316419
#ifndef _WIN32
317420
int DevSession::run_child_once(const fs::path &exePath)
318421
{

0 commit comments

Comments
 (0)