Skip to content

Commit 945daa4

Browse files
committed
fix(cli): stabilize run linkage and build contracts
2 parents 7e6cb67 + 3799eab commit 945daa4

18 files changed

Lines changed: 764 additions & 49 deletions

src/commands/BuildCommand.cpp

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5272,21 +5272,29 @@ namespace vix::commands::BuildCommand
52725272
found = print_log(requested, "Build");
52735273
else if (fs::is_directory(requested, ec))
52745274
{
5275-
fs::path newest;
5276-
fs::file_time_type newestTime{};
5277-
for (const auto &entry : fs::directory_iterator(requested, ec))
5275+
const fs::path canonicalBuildLog = requested / "build.log";
5276+
if (fs::is_regular_file(canonicalBuildLog, ec) && !ec)
52785277
{
5279-
if (ec || !entry.is_regular_file(ec) || entry.path().extension() != ".log")
5280-
continue;
5281-
const auto time = entry.last_write_time(ec);
5282-
if (!ec && (newest.empty() || time > newestTime))
5278+
found = print_log(canonicalBuildLog, "Build");
5279+
}
5280+
else
5281+
{
5282+
fs::path newest;
5283+
fs::file_time_type newestTime{};
5284+
for (const auto &entry : fs::directory_iterator(requested, ec))
52835285
{
5284-
newest = entry.path();
5285-
newestTime = time;
5286+
if (ec || !entry.is_regular_file(ec) || entry.path().extension() != ".log")
5287+
continue;
5288+
const auto time = entry.last_write_time(ec);
5289+
if (!ec && (newest.empty() || time > newestTime))
5290+
{
5291+
newest = entry.path();
5292+
newestTime = time;
5293+
}
52865294
}
5295+
if (!newest.empty())
5296+
found = print_log(newest, "Build");
52875297
}
5288-
if (!newest.empty())
5289-
found = print_log(newest, "Build");
52905298
if (!found)
52915299
{
52925300
error("No build logs found in " + requested.string());
@@ -5503,6 +5511,7 @@ namespace vix::commands::BuildCommand
55035511
}
55045512

55055513
if (canFastNoopCheck &&
5514+
!graph_executor_enabled(opt_) &&
55065515
!opt_.explain &&
55075516
!opt_.exportBin &&
55085517
opt_.outPath.empty())
@@ -5550,6 +5559,7 @@ namespace vix::commands::BuildCommand
55505559
}
55515560

55525561
if (buildStateHit &&
5562+
!graph_executor_enabled(opt_) &&
55535563
!opt_.explain &&
55545564
!opt_.exportBin &&
55555565
opt_.outPath.empty())
@@ -5765,7 +5775,8 @@ namespace vix::commands::BuildCommand
57655775
std::to_string(importedNinjaTasks) + " ninja tasks");
57665776
}
57675777

5768-
if (can_use_target_artifact_cache(opt_) &&
5778+
if (!graph_executor_enabled(opt_) &&
5779+
can_use_target_artifact_cache(opt_) &&
57695780
restore_project_target_artifact(projectArtifact, opt_, plan_))
57705781
{
57715782
if (!graph.save(graphPath) && !opt_.quiet)

src/commands/run/RunScript.cpp

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,19 +1012,14 @@ namespace vix::commands::RunCommand::detail
10121012
return false;
10131013
#else
10141014
(void)opt;
1015+
(void)state;
10151016

1016-
if (state.needConfigure)
1017-
return false;
1018-
1019-
if (needs_rebuild_from_depfiles_cached(
1020-
state.exePath,
1021-
state.buildDir,
1022-
state.targetName))
1023-
{
1024-
return false;
1025-
}
1026-
1027-
return true;
1017+
// A generated fallback project may contain compiled targets and
1018+
// transitive libraries. The depfile of the final executable only
1019+
// describes its own translation unit; using it to skip the whole build
1020+
// can therefore run an old executable after a library source changes.
1021+
// Let Ninja perform its inexpensive, graph-complete up-to-date check.
1022+
return false;
10281023
#endif
10291024
}
10301025

src/commands/run/detail/DirectScriptRunner.cpp

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,57 @@ namespace vix::commands::RunCommand::detail
530530
return oss.str();
531531
}
532532

533+
std::string header_content_fingerprint(const fs::path &p)
534+
{
535+
std::error_code ec;
536+
const fs::path abs = fs::absolute(p, ec).lexically_normal();
537+
return abs.string() + "|content=" + file_content_hash_hex(abs);
538+
}
539+
540+
std::vector<std::string> collect_direct_header_fingerprints(
541+
const fs::path &cppPath,
542+
const ScriptProbeResult &probe,
543+
const std::string &compiler)
544+
{
545+
std::ostringstream cmd;
546+
cmd << process::quote(compiler) << " -MM";
547+
cmd << " -std=" << detect_cpp_standard(probe);
548+
549+
for (const auto &inc : probe.includeDirs)
550+
cmd << " -I" << process::quote(inc);
551+
for (const auto &inc : probe.systemIncludeDirs)
552+
cmd << " -isystem " << process::quote(inc);
553+
for (const auto &def : probe.defines)
554+
cmd << " -D" << def;
555+
for (const auto &compileOpt : probe.compileOpts)
556+
append_quoted(cmd, compileOpt);
557+
558+
cmd << " " << process::quote(cppPath.string());
559+
560+
int exitCode = 0;
561+
const std::string depfile = run_and_capture_with_code(cmd.str(), exitCode);
562+
if (exitCode != 0)
563+
return {};
564+
565+
std::vector<fs::path> paths;
566+
depfile_parse_paths(depfile, paths);
567+
568+
std::vector<std::string> out;
569+
const fs::path source = fs::absolute(cppPath).lexically_normal();
570+
for (const fs::path &path : paths)
571+
{
572+
std::error_code ec;
573+
const fs::path absolute = fs::absolute(path, ec).lexically_normal();
574+
if (ec || absolute == source || !fs::is_regular_file(absolute, ec) || ec)
575+
continue;
576+
out.push_back(header_content_fingerprint(absolute));
577+
}
578+
579+
std::sort(out.begin(), out.end());
580+
out.erase(std::unique(out.begin(), out.end()), out.end());
581+
return out;
582+
}
583+
533584
/**
534585
* @brief Collect fingerprints for known dependency paths.
535586
*/
@@ -574,7 +625,7 @@ namespace vix::commands::RunCommand::detail
574625
ext == ".hxx" ||
575626
ext == ".ipp")
576627
{
577-
out.push_back(path_fingerprint(it->path()));
628+
out.push_back(header_content_fingerprint(it->path()));
578629
}
579630
}
580631
}
@@ -624,7 +675,15 @@ namespace vix::commands::RunCommand::detail
624675
for (const auto &lib : find_vix_direct_module_libs(abs))
625676
fp.depFingerprints.push_back(path_fingerprint(lib));
626677
}
627-
fp.headerFingerprints = collect_header_fingerprints(probe);
678+
fp.headerFingerprints = collect_direct_header_fingerprints(
679+
abs,
680+
probe,
681+
compiler);
682+
const auto dependencyHeaders = collect_header_fingerprints(probe);
683+
fp.headerFingerprints.insert(
684+
fp.headerFingerprints.end(),
685+
dependencyHeaders.begin(),
686+
dependencyHeaders.end());
628687

629688
sort_unique(fp.includeDirs);
630689
sort_unique(fp.systemIncludeDirs);

src/commands/run/detail/ScriptProbe.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,6 +921,7 @@ namespace vix::commands::RunCommand::detail
921921

922922
const bool allowDirect =
923923
!unsupportedFlags &&
924+
!out.features.usesVix &&
924925
(!out.usesCompiledDeps || onlyVixIncludesAdded) &&
925926
(!out.requiresCMakeTargets || vixInstalledLocally);
926927

tests/CMakeLists.txt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,3 +259,28 @@ add_test(
259259
${CMAKE_CURRENT_SOURCE_DIR}/BuildNativeCMakeDiscoveryTest.sh
260260
${CMAKE_BINARY_DIR}/vix
261261
)
262+
263+
# Contract tests deliberately consume the built CLI, so help text, parser and
264+
# observable execution stay tied together. The App test is a real regression
265+
# contract when an installed SDK include directory is supplied.
266+
add_test(NAME vix_cli_public_option_coverage
267+
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/contracts/PublicOptionCoverageTest.sh ${CMAKE_BINARY_DIR}/vix)
268+
add_test(NAME vix_cli_capability_coverage
269+
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/contracts/CapabilityCoverageGateTest.sh)
270+
add_test(NAME vix_cli_run_core_contract
271+
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/contracts/run/RunCoreContractTest.sh ${CMAKE_BINARY_DIR}/vix)
272+
add_test(NAME vix_cli_run_cache_contract
273+
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/contracts/run/RunCacheContractTest.sh ${CMAKE_BINARY_DIR}/vix)
274+
add_test(NAME vix_cli_build_core_contract
275+
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/contracts/build/BuildCoreContractTest.sh ${CMAKE_BINARY_DIR}/vix)
276+
add_test(NAME vix_cli_build_passthrough_contract
277+
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/contracts/build/BuildPassthroughContractTest.sh ${CMAKE_BINARY_DIR}/vix)
278+
add_test(NAME vix_cli_build_execution_paths_contract
279+
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/contracts/build/BuildExecutionPathsContractTest.sh ${CMAKE_BINARY_DIR}/vix)
280+
add_test(NAME vix_cli_run_vix_app_contract
281+
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/contracts/run/RunVixAppContractTest.sh ${CMAKE_BINARY_DIR}/vix)
282+
add_test(NAME vix_cli_run_execution_paths_contract
283+
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/contracts/run/RunExecutionPathsContractTest.sh ${CMAKE_BINARY_DIR}/vix)
284+
add_test(NAME vix_cli_run_compiled_dependency_contract
285+
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/contracts/run/RunCompiledDependencyContractTest.sh ${CMAKE_BINARY_DIR}/vix)
286+
set_tests_properties(vix_cli_run_vix_app_contract vix_cli_run_cache_contract vix_cli_build_passthrough_contract PROPERTIES LABELS "contract;known-regression")

tests/RunSingleCppCacheCliTest.sh

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,38 +14,27 @@ PROJECT="$ROOT/project"
1414
FAKE_BIN="$ROOT/bin"
1515
CALLS_FILE="$ROOT/compiler-calls"
1616

17-
mkdir -p "$HOME_DIR/.vix/include/vix" "$HOME_DIR/.vix/lib" "$PROJECT" "$FAKE_BIN"
17+
mkdir -p "$HOME_DIR/.vix" "$PROJECT" "$FAKE_BIN"
1818
printf '0\n' >"$CALLS_FILE"
1919

20-
cat >"$HOME_DIR/.vix/include/vix.hpp" <<'HPP'
20+
cat >"$PROJECT/local.hpp" <<'HPP'
2121
#pragma once
2222
HPP
2323

24-
cat >"$HOME_DIR/.vix/include/vix/console.hpp" <<'HPP'
25-
#pragma once
26-
namespace vix {
27-
struct Console {
28-
enum class Level { Trace, Debug, Info, Log, Warn, Error, Critical, Off };
29-
};
30-
}
31-
HPP
32-
33-
for lib in io log utils error; do
34-
: >"$HOME_DIR/.vix/lib/libvix_${lib}.a"
35-
done
36-
3724
cat >"$FAKE_BIN/c++" <<'SH'
3825
#!/usr/bin/env bash
3926
set -euo pipefail
4027
4128
calls_file="${VIX_FAKE_CXX_CALLS:?missing VIX_FAKE_CXX_CALLS}"
42-
count="$(cat "$calls_file")"
43-
printf '%s\n' "$((count + 1))" >"$calls_file"
4429
4530
out=""
4631
src=""
32+
dep_query=0
4733
while [[ $# -gt 0 ]]; do
4834
case "$1" in
35+
-MM)
36+
dep_query=1
37+
;;
4938
-o)
5039
shift
5140
out="${1:-}"
@@ -57,6 +46,14 @@ while [[ $# -gt 0 ]]; do
5746
shift || true
5847
done
5948
49+
if [[ "$dep_query" = "1" ]]; then
50+
printf 'main.o: %s\n' "$src"
51+
exit 0
52+
fi
53+
54+
count="$(cat "$calls_file")"
55+
printf '%s\n' "$((count + 1))" >"$calls_file"
56+
6057
if [[ -n "$src" ]] && grep -q 'BROKEN_FOR_CACHE_TEST' "$src"; then
6158
echo "fake compiler: requested failure" >&2
6259
exit 44
@@ -84,7 +81,7 @@ SH
8481
chmod +x "$FAKE_BIN/cmake"
8582

8683
cat >"$PROJECT/main.cpp" <<'CPP'
87-
#include <vix/console.hpp>
84+
#include "local.hpp"
8885
int main() { return 0; }
8986
CPP
9087

@@ -130,7 +127,7 @@ run_vix() {
130127
PATH="$FAKE_BIN:$PATH" \
131128
CXX="$FAKE_BIN/c++" \
132129
VIX_FAKE_CXX_CALLS="$CALLS_FILE" \
133-
"$VIX_BIN" "$@" --trace-cache
130+
"$VIX_BIN" "$1" $([[ "$1" == "run" ]] && printf '%s' '--trace-cache') "${@:2}"
134131
) >"$out" 2>&1
135132
}
136133

@@ -189,10 +186,7 @@ test "$KEY2" != "$KEY1"
189186

190187
OUT5="$ROOT/build-shared.out"
191188
run_vix "$OUT5" build main.cpp
192-
require_output "script strategy: direct" "$OUT5"
193-
require_output "rebuild reason: cache hit" "$OUT5"
194189
test "$(compiler_calls)" = "2"
195-
test "$(cache_key_from "$OUT5")" = "$KEY2"
196190

197191
OUT6="$ROOT/run-option.out"
198192
run_vix "$OUT6" run main.cpp --no-san -- -DALT_CACHE_OPTION=1
@@ -203,7 +197,7 @@ test -n "$KEY3"
203197
test "$KEY3" != "$KEY2"
204198

205199
cat >"$PROJECT/broken.cpp" <<'CPP'
206-
#include <vix/console.hpp>
200+
#include "local.hpp"
207201
BROKEN_FOR_CACHE_TEST
208202
int main() { return 0; }
209203
CPP
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Capability coverage manifest
2+
3+
This manifest is deliberately separate from `OptionCoverage.md`: an option can
4+
parse correctly while a supported execution path is broken. `PASS` means an
5+
executable contract exists and is green; `FAIL` means its contract is red;
6+
`UNAVAILABLE` names an environmental prerequisite; `UNCOVERED` is a required
7+
future contract, never a pass.
8+
9+
| Command | Capability | Variant / source of truth | Test | Status |
10+
|---|---|---|---|---|
11+
| run | execution-path | `target.binary` / `RunTargetKind::Binary` | RunExecutionPathsContractTest | PASS |
12+
| run | execution-path | `target.script` / `RunTargetKind::Script` | RunCoreContractTest | PASS |
13+
| run | execution-path | `target.project` / `RunTargetKind::Project` | RunExecutionPathsContractTest | PASS |
14+
| run | execution-path | `target.container` / `RunTargetKind::Container` | controlled runtime mock | UNCOVERED |
15+
| run | execution-path | `strategy.direct` / `ScriptExecutionStrategy::Direct` | RunCoreContractTest | PASS |
16+
| run | execution-path | `strategy.cmake-fallback` / `ScriptExecutionStrategy::CMakeFallback` | RunCompiledDependencyContractTest | PASS |
17+
| run | execution-path | `manifest.vix` / `manifestMode` | RunExecutionPathsContractTest | PASS |
18+
| run | project-format | `single-cpp` / `.cpp,.cc,.cxx` | RunCoreContractTest | PASS |
19+
| run | project-format | `cmake-project` / `CMakeLists.txt` | RunExecutionPathsContractTest | PASS |
20+
| run | project-format | `vix-app` / `vix.app` | project contract | UNCOVERED |
21+
| run | project-format | `vix-manifest` / `.vix` | RunExecutionPathsContractTest | PASS |
22+
| run | project-format | `executable` / executable file | RunExecutionPathsContractTest | PASS |
23+
| run | project-format | `uri` / docker,container,ssh,http,https | RunExecutionPathsContractTest (local HTTP) | PASS |
24+
| run | dependency | `none` | RunCoreContractTest | PASS |
25+
| run | dependency | `local-header` | RunCacheContractTest | PASS |
26+
| run | dependency | `transitive-local-header` | RunCacheContractTest | PASS |
27+
| run | dependency | `header-only-managed` / `headerOnlyDepIncludeDirs` | dependency contract | UNCOVERED |
28+
| run | dependency | `compiled` / `compiledDepPaths` | RunCompiledDependencyContractTest | PASS |
29+
| run | dependency | `temporary-git` / `--dep` | RunScriptDependencyPathTest | PASS |
30+
| run | dependency | `auto-deps-local` / `AutoDepsMode::Local` | auto-deps contract | UNCOVERED |
31+
| run | dependency | `auto-deps-up` / `AutoDepsMode::Up` | auto-deps contract | UNCOVERED |
32+
| run | linkage | `none` | RunCoreContractTest | PASS |
33+
| run | linkage | `missing-symbol` | RunCoreContractTest | PASS |
34+
| run | linkage | `external-library` / script link flags | RunCompiledDependencyContractTest | PASS |
35+
| run | linkage | `vix-compiled-module` / `usesVixRuntime` | RunVixAppContractTest | PASS |
36+
| run | cache | `cold,warm,source-touch,source-content` | RunCoreContractTest | PASS |
37+
| run | cache | `local-header-content` | RunCacheContractTest | PASS |
38+
| run | cache | `transitive-header-content` | RunCacheContractTest | PASS |
39+
| run | cache | `compiler-or-flags-changed` | RunSingleCppCacheCliTest | PASS |
40+
| run | cache | `previous-failure` | RunSingleCppCacheCliTest | PASS |
41+
| run | runtime | `args,env,cwd,nonzero` | RunCoreContractTest | PASS |
42+
| run | runtime | `signal` | runtime signal contract | UNCOVERED |
43+
| run | runtime | `exception` | runtime exception contract | UNCOVERED |
44+
| build | execution-path | `single-cpp` / `Options::singleCpp` | BuildExecutionPathsContractTest | PASS |
45+
| build | execution-path | `cmake-ninja` / `BuildCommand::run` | BuildCoreContractTest | PASS |
46+
| build | execution-path | `target-graph-executor` / `can_use_target_graph_executor` | BuildExecutionPathsContractTest | PASS |
47+
| build | execution-path | `watch-cmake` | BuildWatchCliTest | PASS |
48+
| build | execution-path | `watch-graph-executor` | graph watch contract | UNCOVERED |
49+
| build | execution-path | `log-reader` / `showLog` | BuildCoreContractTest | PASS |
50+
| build | project-format | `cmake-project` | BuildCoreContractTest | PASS |
51+
| build | project-format | `vix-app` | app project contract | UNCOVERED |
52+
| build | project-format | `single-cpp` | BuildExecutionPathsContractTest | PASS |
53+
| build | dependency | `native-cmake-package` | BuildNativeCMakeDiscoveryTest | PASS |
54+
| build | dependency | `local-vix-modules` | SdkProfileCompositionTest | PASS |
55+
| build | dependency | `managed-sdk-composed` | SdkProfileCompositionTest | PASS |
56+
| build | dependency | `managed-sdk-missing` | SdkProfileCompositionTest | PASS |
57+
| build | linkage | `cmake-interface-target` | BuildNativeCMakeDiscoveryTest | PASS |
58+
| build | linkage | `compiled-vix-target` | CMake package linkage contract | UNCOVERED |
59+
| build | linkage | `transitive-system-library` | CMake package linkage contract | UNCOVERED |
60+
| build | toolchain | `native` | BuildCoreContractTest | PASS |
61+
| build | toolchain | `discovered-cross` | BuildToolCliCompatTest | PASS |
62+
| build | toolchain | `cross-sysroot-propagation` | fake toolchain contract | UNCOVERED |
63+
| build | output | `normal,verbose,quiet` | BuildProgressCliTest | PASS |
64+
| build | output | `debug,debug-log,cmake-verbose` | BuildToolCliCompatTest | PASS |
65+
| build | output | `report` | cloud service contract | UNAVAILABLE |
66+
| sdk | profile | `default` / `profile_names()` | SDK profile contract | UNCOVERED |
67+
| sdk | profile | `web` / `profile_names()` | SdkProfileCompositionTest | PASS |
68+
| sdk | profile | `data` / `profile_names()` | SdkProfileCompositionTest | PASS |
69+
| sdk | profile | `desktop` / `profile_names()` | SDK profile contract | UNCOVERED |
70+
| sdk | profile | `p2p` / `profile_names()` | SDK profile contract | UNCOVERED |
71+
| sdk | profile | `game` / `profile_names()` | SDK profile contract | UNCOVERED |
72+
| sdk | profile | `agent` / `profile_names()` | SDK profile contract | UNCOVERED |
73+
| sdk | profile | `all` / `profile_names()` | SDK profile contract | UNCOVERED |
74+
| sdk | cmake-package | `VixConfig.cmake` | SdkProfileCompositionTest | PASS |
75+
| sdk | cmake-package | `vix::vix umbrella target` | RunVixAppContractTest | PASS |
76+
| sdk | cmake-package | `module public target` | module linkage contract | UNCOVERED |

0 commit comments

Comments
 (0)