From 08e382acbb42a1235005a76f91ef1263be3b3eb7 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 15 Apr 2026 05:38:54 +0300 Subject: [PATCH] builder: fix crun not rebuilding c source (fixes #26876) --- vlib/v/builder/rebuilding.v | 111 ++++++++++++++++++++++-- vlib/v/slow_tests/crun_mode/crun_test.v | 51 ++++++++++- 2 files changed, 150 insertions(+), 12 deletions(-) diff --git a/vlib/v/builder/rebuilding.v b/vlib/v/builder/rebuilding.v index e80a53836..3bdbedf9d 100644 --- a/vlib/v/builder/rebuilding.v +++ b/vlib/v/builder/rebuilding.v @@ -5,11 +5,14 @@ import hash import time import rand import strings +import v.ast import v.util import v.pref import v.vcache import runtime +const crun_cache_format_version = 'crun_cache_v2' + pub fn (mut b Builder) rebuild_modules() { if !b.pref.use_cache || b.pref.build_mode == .build_module { return @@ -294,7 +297,7 @@ pub fn (mut b Builder) should_rebuild() bool { mut cm := vcache.new_cache_manager(b.crun_cache_keys) // always rebuild, when the compilation options changed between 2 sequential cruns: sbuild_options := cm.load('.build_options', '.crun') or { return true } - if sbuild_options != b.pref.build_options.join('\n') { + if sbuild_options != b.crun_build_options_signature() { return true } sdependencies := cm.load('.dependencies', '.crun') or { @@ -302,13 +305,16 @@ pub fn (mut b Builder) should_rebuild() bool { // rebuild, which will fill in the dependencies cache for the next crun return true } - dependencies := sdependencies.split('\n') - // we have already compiled these source files, and have their dependencies - dependencies_stamp := most_recent_timestamp(dependencies) - if dependencies_stamp < exe_stamp { - return false + dependencies := sdependencies.split('\n').filter(it != '') + for dependency in dependencies { + if !os.is_file(dependency) { + return true + } + if os.file_last_mod_unix(dependency) >= exe_stamp { + return true + } } - return true + return false } fn most_recent_timestamp(files []string) i64 { @@ -328,9 +334,9 @@ pub fn (mut b Builder) rebuild(backend_cb FnBackend) { if b.pref.is_crun { // save the dependencies after the first compilation, they will be used for subsequent ones: mut cm := vcache.new_cache_manager(b.crun_cache_keys) - dependency_files := b.parsed_files.map(it.path) + dependency_files := b.crun_dependency_files() cm.save('.dependencies', '.crun', dependency_files.join('\n')) or {} - cm.save('.build_options', '.crun', b.pref.build_options.join('\n')) or {} + cm.save('.build_options', '.crun', b.crun_build_options_signature()) or {} } mut timers := util.get_timers() timers.show_remaining() @@ -384,6 +390,93 @@ pub fn (mut b Builder) rebuild(backend_cb FnBackend) { } } +fn (b &Builder) crun_build_options_signature() string { + mut parts := []string{cap: b.pref.build_options.len + 1} + parts << crun_cache_format_version + parts << b.pref.build_options + return parts.join('\n') +} + +fn add_existing_crun_dependency(mut dependencies map[string]bool, path string) { + if path == '' { + return + } + real_path := os.real_path(path) + if os.is_file(real_path) { + dependencies[real_path] = true + } +} + +fn (b &Builder) crun_hash_stmt_dependency_path(node ast.HashStmt) string { + match node.kind { + 'include', 'preinclude', 'postinclude' { + if node.main.starts_with('<') && node.main.ends_with('>') { + return '' + } + mut path := node.main.trim('"') + if !os.is_abs_path(path) { + path = os.join_path(os.dir(node.source_file), path) + } + return path + } + 'insert' { + mut path := node.main.trim('"') + if !os.is_abs_path(path) { + path = os.join_path(os.dir(node.source_file), path) + } + return path + } + else { + return '' + } + } +} + +fn (b &Builder) collect_crun_stmt_dependencies(mut dependencies map[string]bool, stmt ast.Stmt) { + match stmt { + ast.HashStmt { + add_existing_crun_dependency(mut dependencies, b.crun_hash_stmt_dependency_path(stmt)) + } + ast.ExprStmt { + if stmt.expr is ast.IfExpr && stmt.expr.is_comptime { + b.collect_crun_if_expr_dependencies(mut dependencies, stmt.expr) + } + } + else {} + } +} + +fn (b &Builder) collect_crun_if_expr_dependencies(mut dependencies map[string]bool, expr ast.IfExpr) { + for branch in expr.branches { + for stmt in branch.stmts { + b.collect_crun_stmt_dependencies(mut dependencies, stmt) + } + } +} + +fn (mut b Builder) crun_dependency_files() []string { + mut dependencies := map[string]bool{} + for file in b.parsed_files { + add_existing_crun_dependency(mut dependencies, file.path) + for template_path in file.template_paths { + add_existing_crun_dependency(mut dependencies, template_path) + } + for embedded_file in file.embedded_files { + add_existing_crun_dependency(mut dependencies, embedded_file.apath) + } + for stmt in file.stmts { + b.collect_crun_stmt_dependencies(mut dependencies, stmt) + } + } + for cflag in b.get_os_cflags() { + value := cflag.eval() or { continue } + add_existing_crun_dependency(mut dependencies, value) + } + mut files := dependencies.keys() + files.sort() + return files +} + pub fn (mut b Builder) get_vtmp_filename(base_file_name string, postfix string) string { vtmp := os.vtmp_dir() mut uniq := '' diff --git a/vlib/v/slow_tests/crun_mode/crun_test.v b/vlib/v/slow_tests/crun_mode/crun_test.v index 6b5640801..aee04cb1a 100644 --- a/vlib/v/slow_tests/crun_mode/crun_test.v +++ b/vlib/v/slow_tests/crun_mode/crun_test.v @@ -28,7 +28,7 @@ fn test_crun_simple_v_program_several_times() { mut sw := time.new_stopwatch() mut times := []i64{} for i in 0 .. 10 { - vcrun() + vcrun(vprogram_file) times << sw.elapsed().microseconds() time.sleep(50 * time.millisecond) sw.restart() @@ -41,10 +41,55 @@ fn test_crun_simple_v_program_several_times() { } } -fn vcrun() { - cmd := '${os.quoted_path(vexe)} crun ${os.quoted_path(vprogram_file)}' +fn test_crun_rebuilds_when_local_c_source_changes() { + module_dir := os.join_path(crun_folder, 'c_source_module') + main_file := os.join_path(module_dir, 'code_tests.v') + os.mkdir_all(module_dir)! + os.write_file(os.join_path(module_dir, 'v.mod'), "Module {\n\tname: 'c_source_module'\n}\n")! + os.write_file(main_file, [ + 'module main', + '', + '#include "@VMODROOT/code.c"', + '', + '@[keep_args_alive]', + 'fn C.foo(arg [4]int)', + '', + 'fn main() {', + '\tC.foo([1, 2, 3, 4]!)', + '}', + ].join('\n'))! + write_c_source_module(module_dir, 'OLD', 2)! + // `crun` cache invalidation uses second-resolution mtimes. + time.sleep(1100 * time.millisecond) + first := vcrun(module_dir) + assert first.output == 'OLD:0:1\nOLD:1:2\n' + time.sleep(1100 * time.millisecond) + write_c_source_module(module_dir, 'NEW', 4)! + second := vcrun(module_dir) + assert second.output == 'NEW:0:1\nNEW:1:2\nNEW:2:3\nNEW:3:4\n' +} + +fn write_c_source_module(module_dir string, prefix string, count int) ! { + os.write_file(os.join_path(module_dir, 'code.c'), [ + '#include ', + '', + 'void foo(int arg[4]) {', + '\tfor (int i = 0; i < ${count}; ++i) {', + '\t\tprintf("${prefix}:%d:%d\\n", i, arg[i]);', + '\t}', + '}', + ].join('\n'))! +} + +fn vcrun(target string) os.Result { + cmd := '${os.quoted_path(vexe)} crun ${os.quoted_path(target)}' eprintln('now: ${time.now().format_ss_milli()} | cmd: ${cmd}') res := os.execute(cmd) assert res.exit_code == 0 + return res +} + +fn test_crun_simple_v_program_output() { + res := vcrun(vprogram_file) assert res.output == 'hello' } -- 2.39.5