Skip to content

Commit cca558c

Browse files
committed
fix(run): resolve runnable executables generically
1 parent 42fc691 commit cca558c

5 files changed

Lines changed: 340 additions & 140 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
*
3+
* @file RunnableExecutableResolver.hpp
4+
* @author Gaspard Kirira
5+
*
6+
* Copyright 2025, Gaspard Kirira. All rights reserved.
7+
* https://github.com/vixcpp/vix
8+
* Use of this source code is governed by a MIT license
9+
* that can be found in the License file.
10+
*
11+
* Vix.cpp
12+
*
13+
*/
14+
#ifndef VIX_RUN_RUNNABLE_EXECUTABLE_RESOLVER_HPP
15+
#define VIX_RUN_RUNNABLE_EXECUTABLE_RESOLVER_HPP
16+
17+
#include <filesystem>
18+
#include <optional>
19+
#include <string>
20+
#include <vector>
21+
22+
namespace vix::commands::RunCommand::detail
23+
{
24+
/**
25+
* @brief Find executable files produced inside a CMake build directory.
26+
*
27+
* This function scans the build directory recursively and returns files that
28+
* are runnable on the current platform. CMake internal folders and Vix cache
29+
* folders are ignored.
30+
*
31+
* @param buildDir Build directory to scan.
32+
* @param includeTests Whether test binaries should be included.
33+
* @return List of runnable executable paths.
34+
*/
35+
std::vector<std::filesystem::path> find_runnable_executables(
36+
const std::filesystem::path &buildDir,
37+
bool includeTests = false);
38+
39+
/**
40+
* @brief Resolve the executable that should be run from a build directory.
41+
*
42+
* If a preferred target name is provided, the resolver first tries to match an
43+
* executable with that name. If no preferred name is provided, or no match is
44+
* found, the resolver returns the only runnable executable when exactly one
45+
* exists.
46+
*
47+
* When multiple runnable executables exist and no unique target can be chosen,
48+
* this function returns std::nullopt.
49+
*
50+
* @param buildDir Build directory to scan.
51+
* @param preferredName Optional executable or target name to prefer.
52+
* @return Resolved executable path, or std::nullopt if resolution is ambiguous.
53+
*/
54+
std::optional<std::filesystem::path> resolve_runnable_executable(
55+
const std::filesystem::path &buildDir,
56+
const std::string &preferredName = {});
57+
58+
/**
59+
* @brief Print runnable executable candidates for the user.
60+
*
61+
* The output is intended for diagnostics when Vix cannot automatically choose
62+
* one executable from a build directory.
63+
*
64+
* @param buildDir Build directory to scan.
65+
*/
66+
void print_runnable_executable_candidates(
67+
const std::filesystem::path &buildDir);
68+
69+
/**
70+
* @brief Return a user-facing path for a runnable executable.
71+
*
72+
* The resolver prefers a path relative to the current working directory when
73+
* possible. If that cannot be computed, it falls back to the absolute path.
74+
*
75+
* @param p Executable path.
76+
* @return Display path suitable for CLI hints.
77+
*/
78+
std::string runnable_executable_display_path(
79+
const std::filesystem::path &p);
80+
}
81+
82+
#endif // VIX_RUN_RUNNABLE_EXECUTABLE_RESOLVER_HPP

src/commands/BuildCommand.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3261,7 +3261,7 @@ namespace vix::commands::BuildCommand
32613261
plan_.cmakeSourceDir / "CMakeLists.txt",
32623262
"CMake configure failed");
32633263

3264-
if (opt_.verbose && !log.empty())
3264+
if (!handled && opt_.verbose && !log.empty())
32653265
{
32663266
std::cerr << "\nCMake output:\n";
32673267
std::cerr << log << "\n";

src/commands/RunCommand.cpp

Lines changed: 60 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <vix/cli/manifest/RunManifestMerge.hpp>
2020
#include <vix/cli/manifest/VixManifest.hpp>
2121
#include <vix/cli/app/AppProjectResolver.hpp>
22+
#include <vix/cli/commands/run/detail/RunnableExecutableResolver.hpp>
2223
#include <vix/cli/Style.hpp>
2324
#include <vix/utils/Env.hpp>
2425

@@ -177,130 +178,6 @@ namespace
177178
return out;
178179
}
179180

180-
static std::vector<fs::path> find_runnable_executables(
181-
const fs::path &buildDir,
182-
bool includeTests = false)
183-
{
184-
std::vector<fs::path> candidates;
185-
186-
auto is_executable_candidate = [](const fs::path &p) -> bool
187-
{
188-
std::error_code ec{};
189-
190-
if (!fs::is_regular_file(p, ec) || ec)
191-
return false;
192-
193-
#ifdef _WIN32
194-
return p.extension() == ".exe";
195-
#else
196-
const auto perms = fs::status(p, ec).permissions();
197-
if (ec)
198-
return false;
199-
200-
using pr = fs::perms;
201-
return (perms & pr::owner_exec) != pr::none ||
202-
(perms & pr::group_exec) != pr::none ||
203-
(perms & pr::others_exec) != pr::none;
204-
#endif
205-
};
206-
207-
auto looks_like_test_binary = [](const fs::path &p) -> bool
208-
{
209-
const std::string n = p.filename().string();
210-
return n.find("_test") != std::string::npos ||
211-
n.find("_tests") != std::string::npos ||
212-
n.rfind("test_", 0) == 0;
213-
};
214-
215-
std::error_code ec{};
216-
if (!fs::exists(buildDir, ec) || ec)
217-
return candidates;
218-
219-
for (auto it = fs::recursive_directory_iterator(
220-
buildDir,
221-
fs::directory_options::skip_permission_denied,
222-
ec);
223-
!ec && it != fs::recursive_directory_iterator();
224-
++it)
225-
{
226-
const fs::path p = it->path();
227-
228-
const std::string pathString = p.string();
229-
230-
if (pathString.find("CMakeFiles") != std::string::npos)
231-
continue;
232-
233-
if (pathString.find(".vix") != std::string::npos)
234-
continue;
235-
236-
if (!is_executable_candidate(p))
237-
continue;
238-
239-
if (!includeTests && looks_like_test_binary(p))
240-
continue;
241-
242-
candidates.push_back(p);
243-
}
244-
245-
auto prefer_short_bin_path = [](const fs::path &a, const fs::path &b) -> bool
246-
{
247-
const bool aBin = a.string().find("/bin/") != std::string::npos ||
248-
a.string().find("\\bin\\") != std::string::npos;
249-
250-
const bool bBin = b.string().find("/bin/") != std::string::npos ||
251-
b.string().find("\\bin\\") != std::string::npos;
252-
253-
if (aBin != bBin)
254-
return aBin;
255-
256-
return a.string().size() < b.string().size();
257-
};
258-
259-
std::sort(candidates.begin(), candidates.end(), prefer_short_bin_path);
260-
return candidates;
261-
}
262-
263-
static std::optional<fs::path> resolve_runnable_executable(
264-
const fs::path &buildDir,
265-
const std::string &preferredName = {})
266-
{
267-
const auto candidates = find_runnable_executables(buildDir, false);
268-
269-
if (!preferredName.empty())
270-
{
271-
for (const auto &p : candidates)
272-
{
273-
#ifdef _WIN32
274-
const std::string name = p.stem().string();
275-
#else
276-
const std::string name = p.filename().string();
277-
#endif
278-
279-
if (name == preferredName)
280-
return p;
281-
}
282-
}
283-
284-
if (candidates.size() == 1)
285-
return candidates.front();
286-
287-
return std::nullopt;
288-
}
289-
290-
static void print_runnable_executable_candidates(
291-
const fs::path &buildDir)
292-
{
293-
const auto candidates = find_runnable_executables(buildDir, false);
294-
295-
if (candidates.empty())
296-
return;
297-
298-
hint("Runnable executables found:");
299-
300-
for (const auto &p : candidates)
301-
step(" " + p.filename().string());
302-
}
303-
304181
static int build_project_with_vix_build(
305182
const fs::path &projectDir,
306183
const Options &opt,
@@ -382,7 +259,7 @@ namespace
382259
return buildExit;
383260

384261
auto exePath =
385-
resolve_runnable_executable(
262+
detail::resolve_runnable_executable(
386263
buildDir,
387264
requestedTarget);
388265

@@ -392,11 +269,11 @@ namespace
392269
{
393270
error("Built executable not found for target: " + requestedTarget);
394271
hint("Resolved build directory: " + buildDir.string());
395-
print_runnable_executable_candidates(buildDir);
272+
detail::print_runnable_executable_candidates(buildDir);
396273
return 1;
397274
}
398275

399-
const auto candidates = find_runnable_executables(buildDir, false);
276+
const auto candidates = detail::find_runnable_executables(buildDir, false);
400277

401278
if (candidates.empty())
402279
{
@@ -410,7 +287,7 @@ namespace
410287
hint("Run one explicitly:");
411288

412289
for (const auto &p : candidates)
413-
step(" vix run " + p.filename().string());
290+
step(" vix run " + detail::runnable_executable_display_path(p));
414291

415292
return 1;
416293
}
@@ -1145,6 +1022,28 @@ namespace
11451022

11461023
if (!exePathOpt)
11471024
{
1025+
const auto candidates = find_runnable_executables(buildDir, false);
1026+
1027+
if (candidates.size() > 1)
1028+
{
1029+
error("Multiple runnable executables found.");
1030+
hint("Resolved build directory: " + buildDir.string());
1031+
hint("Run one explicitly:");
1032+
1033+
for (const auto &p : candidates)
1034+
{
1035+
std::error_code relEc;
1036+
fs::path rel = fs::relative(p, fs::current_path(), relEc);
1037+
1038+
const std::string runnable =
1039+
relEc ? p.string() : rel.string();
1040+
1041+
step(" vix run " + runnable);
1042+
}
1043+
1044+
return 1;
1045+
}
1046+
11481047
const int testRc = run_test_binary_if_present(
11491048
buildDir,
11501049
opt.runArgs,
@@ -1165,14 +1064,15 @@ namespace
11651064
error("Built executable not found for target: " + exeName);
11661065
else
11671066
error("Built executable not found.");
1067+
11681068
hint("Resolved build directory: " + buildDir.string());
1069+
hint("No runnable application target could be resolved automatically.");
11691070

11701071
if (resolved.generated)
11711072
hint("Generated CMake source: " + resolved.cmakeSourceDir.string());
11721073

11731074
return 1;
11741075
}
1175-
11761076
const int runRc = run_executable_direct(
11771077
*exePathOpt,
11781078
opt,
@@ -1358,11 +1258,37 @@ namespace
13581258

13591259
if (!exePathOpt)
13601260
{
1361-
error("Built executable not found for project: " + exeName);
1362-
hint("Resolved build directory: " + buildDir.string());
1363-
hint("No runnable application target could be resolved automatically.");
1364-
hint("If your executable uses a custom output path or custom target name, add a manifest field to specify it.");
1365-
return 1;
1261+
const auto candidates = find_runnable_executables(buildDir, false);
1262+
1263+
if (candidates.empty())
1264+
{
1265+
error("Built executable not found.");
1266+
hint("Resolved build directory: " + buildDir.string());
1267+
hint("No runnable application target could be resolved automatically.");
1268+
return 1;
1269+
}
1270+
1271+
if (candidates.size() > 1)
1272+
{
1273+
error("Multiple runnable executables found.");
1274+
hint("Resolved build directory: " + buildDir.string());
1275+
hint("Run one explicitly:");
1276+
1277+
for (const auto &p : candidates)
1278+
{
1279+
std::error_code ec;
1280+
fs::path rel = fs::relative(p, fs::current_path(), ec);
1281+
1282+
const std::string runnable =
1283+
ec ? p.string() : rel.string();
1284+
1285+
step(" vix run " + runnable);
1286+
}
1287+
1288+
return 1;
1289+
}
1290+
1291+
exePathOpt = candidates.front();
13661292
}
13671293

13681294
fs::path exePath = *exePathOpt;

src/commands/run/RunScript.cpp

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include <vix/cli/util/Ui.hpp>
2525
#include <vix/utils/Env.hpp>
2626
#include <vix/cli/commands/run/dev/DevSession.hpp>
27+
#include <vix/cli/commands/run/detail/RunnableExecutableResolver.hpp>
2728

2829
#include <algorithm>
2930
#include <cerrno>
@@ -2133,16 +2134,31 @@ namespace vix::commands::RunCommand::detail
21332134
watch_spinner_finish();
21342135
}
21352136

2136-
const std::string exeName = projectDir.filename().string();
2137-
fs::path exePath = buildDir / exeName;
2137+
auto exePathOpt = resolve_runnable_executable(buildDir);
21382138

2139-
if (!fs::exists(exePath))
2139+
if (!exePathOpt)
21402140
{
2141-
error("Dev executable not found in build-dev/: " + exePath.string());
2142-
hint("Make sure your CMakeLists.txt defines an executable named '" + exeName + "'.");
2141+
const auto candidates = find_runnable_executables(buildDir, false);
2142+
2143+
if (candidates.empty())
2144+
{
2145+
error("No runnable executable found.");
2146+
hint("Resolved build directory: " + buildDir.string());
2147+
hint("The project may only build libraries, tests, or non-runnable targets.");
2148+
return 1;
2149+
}
2150+
2151+
error("Multiple runnable executables found.");
2152+
hint("Resolved build directory: " + buildDir.string());
2153+
hint("Run one explicitly:");
2154+
2155+
for (const auto &p : candidates)
2156+
step(" vix run " + runnable_executable_display_path(p));
2157+
21432158
return 1;
21442159
}
21452160

2161+
fs::path exePath = *exePathOpt;
21462162
const auto childStart = Clock::now();
21472163

21482164
pid_t pid = ::fork();

0 commit comments

Comments
 (0)