@@ -79,8 +79,10 @@ namespace vix::commands::RunCommand::detail
7979 fs::path buildDir;
8080 fs::path exePath;
8181 fs::path sigFile;
82+ fs::path graphFingerprintFile;
8283 fs::path configureLogPath;
8384 fs::path buildLogPath;
85+ std::vector<fs::path> dependencyRoots;
8486
8587 bool useVixRuntime = false ;
8688 bool needConfigure = true ;
@@ -101,15 +103,170 @@ namespace vix::commands::RunCommand::detail
101103 state.buildDir = plan.buildDir ;
102104 state.exePath = plan.exePath ;
103105 state.sigFile = plan.signatureFile ;
106+ state.graphFingerprintFile = plan.projectDir / " .vix-build.graph" ;
104107 state.configureLogPath = plan.configureLogPath ;
105108 state.buildLogPath = plan.buildLogPath ;
109+ state.dependencyRoots = plan.dependencyRoots ;
106110 state.useVixRuntime = plan.useVixRuntime ;
107111 state.needConfigure = plan.shouldConfigure ;
108112 state.skipBuild = !plan.shouldBuild ;
109113 state.configSignature = plan.configSignature ;
110114 return state;
111115 }
112116
117+ std::string cmake_graph_path_fingerprint (const fs::path &path)
118+ {
119+ std::error_code ec;
120+ const fs::path absolute = fs::absolute (path, ec).lexically_normal ();
121+ if (ec || !fs::is_regular_file (absolute, ec) || ec)
122+ return {};
123+
124+ const auto size = fs::file_size (absolute, ec);
125+ if (ec)
126+ return {};
127+ const auto mtime = fs::last_write_time (absolute, ec);
128+ if (ec)
129+ return {};
130+ return absolute.string () + " |" + std::to_string (size) + " |" +
131+ std::to_string (mtime.time_since_epoch ().count ());
132+ }
133+
134+ std::string cmake_graph_content_fingerprint (const fs::path &path)
135+ {
136+ const std::string identity = cmake_graph_path_fingerprint (path);
137+ if (identity.empty ())
138+ return {};
139+ return identity + " |content=" +
140+ std::to_string (std::hash<std::string>{}(text::read_text_file_or_empty (path)));
141+ }
142+
143+ void append_cmake_depfile_inputs (
144+ const fs::path &depfile,
145+ std::vector<std::string> &inputs)
146+ {
147+ std::string depContents = text::read_text_file_or_empty (depfile);
148+ for (std::size_t pos = 0 ; (pos = depContents.find (" \\\\\n " , pos)) != std::string::npos;)
149+ depContents.replace (pos, 2 , " " );
150+ const auto colon = depContents.find (' :' );
151+ if (colon == std::string::npos)
152+ return ;
153+ std::istringstream stream (depContents.substr (colon + 1 ));
154+ std::string token;
155+ while (stream >> token)
156+ {
157+ if (const std::string fp = cmake_graph_path_fingerprint (token); !fp.empty ())
158+ inputs.push_back (std::move (fp));
159+ }
160+ }
161+
162+ std::string make_cmake_graph_fingerprint (const ScriptProjectState &state)
163+ {
164+ std::vector<std::string> inputs;
165+ inputs.reserve (256 );
166+ for (const fs::path &path : {state.script , state.cmakeLists , state.buildDir / " build.ninja" , state.buildDir / " CMakeCache.txt" })
167+ {
168+ if (const std::string fp = cmake_graph_path_fingerprint (path); !fp.empty ())
169+ inputs.push_back (std::move (fp));
170+ }
171+
172+ std::error_code ec;
173+ const fs::path cmakeFiles = state.buildDir / " CMakeFiles" ;
174+ if (fs::exists (cmakeFiles, ec) && !ec)
175+ {
176+ for (fs::recursive_directory_iterator it (cmakeFiles, ec), end; !ec && it != end; it.increment (ec))
177+ {
178+ if (!it->is_regular_file (ec) || it->path ().extension () != " .d" )
179+ continue ;
180+ append_cmake_depfile_inputs (it->path (), inputs);
181+ }
182+ }
183+
184+ // Dependency roots originate from manifest/registry metadata collected by
185+ // ScriptProbe. Restrict the scan to build inputs, never the caller's
186+ // whole repository.
187+ std::vector<fs::path> dependencyRoots = state.dependencyRoots ;
188+ dependencyRoots.push_back (state.script .parent_path () / " .vix" / " deps" );
189+ for (const auto &root : dependencyRoots)
190+ {
191+ if (!fs::exists (root, ec) || ec)
192+ {
193+ ec.clear ();
194+ continue ;
195+ }
196+ for (fs::recursive_directory_iterator it (
197+ root, fs::directory_options::skip_permission_denied, ec),
198+ end;
199+ !ec && it != end;
200+ it.increment (ec))
201+ {
202+ if (it->is_directory (ec) && it->path ().filename () == " .git" )
203+ {
204+ it.disable_recursion_pending ();
205+ continue ;
206+ }
207+ if (!it->is_regular_file (ec))
208+ continue ;
209+ const std::string name = it->path ().filename ().string ();
210+ const std::string ext = it->path ().extension ().string ();
211+ if (name == " CMakeLists.txt" || name == " vix.lock" || name == " vix_deps.cmake" ||
212+ ext == " .cpp" || ext == " .cc" || ext == " .cxx" || ext == " .c" ||
213+ ext == " .h" || ext == " .hpp" || ext == " .hh" || ext == " .hxx" || ext == " .ipp" )
214+ {
215+ if (const std::string fp = cmake_graph_content_fingerprint (it->path ()); !fp.empty ())
216+ inputs.push_back (std::move (fp));
217+ }
218+ }
219+ ec.clear ();
220+ }
221+
222+ // Imported libraries are explicit order-only inputs of Ninja link edges.
223+ // Fingerprint these artifacts without asking Ninja to re-evaluate the graph.
224+ const std::string ninja = text::read_text_file_or_empty (state.buildDir / " build.ninja" );
225+ std::istringstream ninjaLines (ninja);
226+ std::string line;
227+ while (std::getline (ninjaLines, line))
228+ {
229+ const auto pipe = line.find (" | " );
230+ if (pipe == std::string::npos)
231+ continue ;
232+ std::istringstream paths (line.substr (pipe + 3 ));
233+ std::string path;
234+ while (paths >> path)
235+ {
236+ if (const std::string fp = cmake_graph_path_fingerprint (path); !fp.empty ())
237+ inputs.push_back (std::move (fp));
238+ }
239+ }
240+
241+ // A CONFIG package can change usage requirements without changing the
242+ // application source. CMakeCache records the resolved package directories.
243+ std::istringstream cacheLines (text::read_text_file_or_empty (state.buildDir / " CMakeCache.txt" ));
244+ while (std::getline (cacheLines, line))
245+ {
246+ const auto marker = line.find (" :PATH=" );
247+ if (marker == std::string::npos || line.rfind (" _DIR" , marker) != marker - 4 )
248+ continue ;
249+ const fs::path packageDir = line.substr (marker + 6 );
250+ for (fs::directory_iterator it (packageDir, ec), end; !ec && it != end; it.increment (ec))
251+ {
252+ if (it->is_regular_file (ec) && it->path ().extension () == " .cmake" )
253+ {
254+ if (const std::string fp = cmake_graph_path_fingerprint (it->path ()); !fp.empty ())
255+ inputs.push_back (std::move (fp));
256+ }
257+ }
258+ ec.clear ();
259+ }
260+
261+ std::sort (inputs.begin (), inputs.end ());
262+ inputs.erase (std::unique (inputs.begin (), inputs.end ()), inputs.end ());
263+ std::ostringstream result;
264+ result << " config=" << state.configSignature << ' \n ' ;
265+ for (const auto &input : inputs)
266+ result << input << ' \n ' ;
267+ return result.str ();
268+ }
269+
113270 bool project_has_registry_lock_dependencies (const fs::path &projectDir)
114271 {
115272 const fs::path lockPath = projectDir / " vix.lock" ;
@@ -1012,13 +1169,16 @@ namespace vix::commands::RunCommand::detail
10121169 return false ;
10131170#else
10141171 (void )opt;
1015- (void )state;
1172+ if (state.needConfigure || !fs::exists (state.exePath ))
1173+ return false ;
10161174
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.
1175+ // The stored graph includes depfiles for every compiled target and the
1176+ // imported link artifacts, rather than only the final executable's TU.
1177+ const std::string previous = text::read_text_file_or_empty (state.graphFingerprintFile );
1178+ const bool hit = !previous.empty () && previous == make_cmake_graph_fingerprint (state);
1179+ if (opt.traceCache )
1180+ std::cerr << " cmake graph cache: miss (graph validation pending)\n " ;
1181+ (void )hit;
10221182 return false ;
10231183#endif
10241184 }
@@ -1050,7 +1210,11 @@ namespace vix::commands::RunCommand::detail
10501210 code = normalize_exit_code (code);
10511211
10521212 if (code == 0 )
1213+ {
1214+ (void )text::write_text_file (state.graphFingerprintFile ,
1215+ make_cmake_graph_fingerprint (state));
10531216 return 0 ;
1217+ }
10541218
10551219 std::ifstream ifs (state.buildLogPath );
10561220 std::string logContent;
@@ -1370,6 +1534,7 @@ namespace vix::commands::RunCommand::detail
13701534 opt.withSqlite ,
13711535 opt.withMySql );
13721536
1537+ if (text::read_text_file_or_empty (state.cmakeLists ) != cmakeText)
13731538 {
13741539 std::ofstream out (state.cmakeLists , std::ios::trunc);
13751540 if (!out)
@@ -1672,6 +1837,13 @@ namespace vix::commands::RunCommand::detail
16721837 opt.scriptFlags ,
16731838 opt.withSqlite ,
16741839 opt.withMySql );
1840+ plan.dependencyRoots = probe.compiledDepPaths ;
1841+ // ScriptCMake can discover manifest dependencies independently of the
1842+ // include probe. Include that metadata-owned root in the graph snapshot.
1843+ const fs::path manifestDeps = plan.scriptPath .parent_path () / " .vix" / " deps" ;
1844+ std::error_code depsEc;
1845+ if (fs::exists (manifestDeps, depsEc) && !depsEc)
1846+ plan.dependencyRoots .push_back (manifestDeps);
16751847
16761848 plan.shouldConfigure = true ;
16771849 plan.shouldBuild = true ;
0 commit comments