module builder import os 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 } if b.pref.check_only || b.pref.only_check_syntax { // Check-only flows should not trigger side-effecting cache rebuilds. // In the REPL import path that can compile imported `.c.v` modules // and surface irrelevant missing-header errors for declaration-only checks. return } all_files := b.parsed_files.map(it.path) $if trace_invalidations ? { eprintln('> rebuild_modules all_files: ${all_files}') } invalidations := b.find_invalidated_modules_by_files(all_files) $if trace_invalidations ? { eprintln('> rebuild_modules invalidations: ${invalidations}') } if invalidations.len > 0 { vexe := pref.vexe_path() for imp in invalidations { b.v_build_module(vexe, imp) } } } pub fn (mut b Builder) find_invalidated_modules_by_files(all_files []string) []string { util.timing_start('${@METHOD} source_hashing') mut new_hashes := map[string]string{} mut old_hashes := map[string]string{} mut sb_new_hashes := strings.new_builder(1024) mut cm := vcache.new_cache_manager(all_files) sold_hashes := cm.load('.hashes', 'all_files') or { ' ' } // eprintln(sold_hashes) sold_hashes_lines := sold_hashes.split('\n') for line in sold_hashes_lines { if line.len == 0 { continue } x := line.split(' ') chash := x[0] cpath := x[1] old_hashes[cpath] = chash } // eprintln('old_hashes: ${old_hashes}') for cpath in all_files { ccontent := util.read_file(cpath) or { '' } chash := hash.sum64_string(ccontent, 7).hex_full() new_hashes[cpath] = chash sb_new_hashes.write_string(chash) sb_new_hashes.write_u8(` `) sb_new_hashes.write_string(cpath) sb_new_hashes.write_u8(`\n`) } snew_hashes := sb_new_hashes.str() // eprintln('new_hashes: ${new_hashes}') // eprintln('> new_hashes != old_hashes: ' + ( old_hashes != new_hashes ).str()) // eprintln(snew_hashes) cm.save('.hashes', 'all_files', snew_hashes) or {} util.timing_measure('${@METHOD} source_hashing') mut invalidations := []string{} if new_hashes != old_hashes { util.timing_start('${@METHOD} rebuilding') // eprintln('> b.mod_invalidates_paths: ${b.mod_invalidates_paths}') // eprintln('> b.mod_invalidates_mods: ${b.mod_invalidates_mods}') // eprintln('> b.path_invalidates_mods: ${b.path_invalidates_mods}') $if trace_invalidations ? { for k, v in b.mod_invalidates_paths { mut m := map[string]bool{} for mm in b.mod_invalidates_mods[k] { m[mm] = true } eprintln('> module `${k}` invalidates: ${m.keys()}') for fpath in v { eprintln(' ${fpath}') } } } mut invalidated_paths := map[string]int{} mut invalidated_mod_paths := map[string]int{} for npath, nhash in new_hashes { if npath !in old_hashes { invalidated_paths[npath]++ continue } if old_hashes[npath] != nhash { invalidated_paths[npath]++ continue } } for opath, ohash in old_hashes { if opath !in new_hashes { invalidated_paths[opath]++ continue } if new_hashes[opath] != ohash { invalidated_paths[opath]++ continue } } $if trace_invalidations ? { eprintln('invalidated_paths: ${invalidated_paths}') } mut rebuild_everything := false for cycle := 0; true; cycle++ { $if trace_invalidations ? { eprintln('> cycle: ${cycle} | invalidated_paths: ${invalidated_paths}') } mut new_invalidated_paths := map[string]int{} for npath, _ in invalidated_paths { invalidated_mods := b.path_invalidates_mods[npath] if invalidated_mods == ['main'] { continue } if 'builtin' in invalidated_mods { // When `builtin` is invalid, there is no point in // extracting a finer grained dependency resolution // of the dependencies any more. Instead, just rebuild // every module. rebuild_everything = true break } for imod in invalidated_mods { if imod == 'main' { continue } for np in b.mod_invalidates_paths[imod] { new_invalidated_paths[np]++ } } $if trace_invalidations ? { eprintln('> npath -> invalidated_mods | ${npath} -> ${invalidated_mods}') } mpath := os.dir(npath) invalidated_mod_paths[mpath]++ } if rebuild_everything { break } if new_invalidated_paths.len == 0 { break } invalidated_paths = new_invalidated_paths.clone() } if rebuild_everything { invalidated_mod_paths = {} for npath, _ in new_hashes { mpath := os.dir(npath) pimods := b.path_invalidates_mods[npath] if pimods == ['main'] { continue } invalidated_mod_paths[mpath]++ } } $if trace_invalidations ? { eprintln('invalidated_mod_paths: ${invalidated_mod_paths}') eprintln('rebuild_everything: ${rebuild_everything}') } if invalidated_mod_paths.len > 0 { impaths := invalidated_mod_paths.keys() for imp in impaths { invalidations << imp } } util.timing_measure('${@METHOD} rebuilding') } return invalidations } fn (mut b Builder) v_build_module(vexe string, imp_path string) { pwd := os.getwd() defer { os.chdir(pwd) or {} } // do run `v build-module x` always in main vfolder; x can be a relative path vroot := os.dir(vexe) os.chdir(vroot) or {} boptions := b.pref.build_options.join(' ') rebuild_cmd := '${os.quoted_path(vexe)} ${boptions} build-module ${os.quoted_path(imp_path)}' vcache.dlog('| Builder.' + @FN, 'vexe: ${vexe} | imp_path: ${imp_path} | rebuild_cmd: ${rebuild_cmd}') $if trace_v_build_module ? { eprintln('> Builder.v_build_module: ${rebuild_cmd}') } // eprintln('> Builder.v_build_module: ${rebuild_cmd}') os.system(rebuild_cmd) } fn (mut b Builder) rebuild_cached_module(vexe string, imp_path string) string { res := b.pref.cache_manager.mod_exists(imp_path, '.o', imp_path) or { if b.pref.is_verbose { println('Cached ${imp_path} .o file not found... Building .o file for ${imp_path}') } b.v_build_module(vexe, imp_path) rebuilt_o := b.pref.cache_manager.mod_exists(imp_path, '.o', imp_path) or { panic('could not rebuild cache module for ${imp_path}, error: ${err.msg()}') } return rebuilt_o } return res } fn (mut b Builder) handle_usecache(vexe string) { if !b.pref.use_cache || b.pref.build_mode == .build_module { return } mut libs := []string{} // builtin.o os.o http.o etc mut built_modules := []string{} builtin_obj_path := b.rebuild_cached_module(vexe, 'vlib/builtin') libs << builtin_obj_path for ast_file in b.parsed_files { if b.pref.is_test && ast_file.mod.name != 'main' { imp_path := b.find_module_path(ast_file.mod.name, ast_file.path) or { verror('cannot import module "${ast_file.mod.name}" (not found)') break } obj_path := b.rebuild_cached_module(vexe, imp_path) libs << obj_path built_modules << ast_file.mod.name } for imp_stmt in ast_file.imports { imp := imp_stmt.mod // strconv is already imported inside builtin, so skip generating its object file // TODO: in case we have other modules with the same name, make sure they are vlib // is this even doing anything? if util.module_is_builtin(imp) { continue } if imp in built_modules { continue } if util.should_bundle_module(imp) { continue } // The problem is cmd/v is in module main and imports // the relative module named help, which is built as cmd.v.help not help // currently this got this working by building into main, see ast.FnDecl in cgen if imp == 'help' { continue } imp_path := b.find_module_path(imp, ast_file.path) or { verror('cannot import module "${imp}" (not found)') break } obj_path := b.rebuild_cached_module(vexe, imp_path) libs << obj_path built_modules << imp } } b.ccoptions.post_args << libs } pub fn (mut b Builder) should_rebuild() bool { mut exe_name := b.pref.out_name $if windows { exe_name += '.exe' } if !os.is_file(exe_name) { return true } if !b.pref.is_crun { return true } mut v_program_files := []string{} is_file := os.is_file(b.pref.path) is_dir := os.is_dir(b.pref.path) if is_file { v_program_files << b.pref.path } else if is_dir { v_program_files << b.v_files_from_dir(b.pref.path) } v_program_files.sort() // ensure stable keys for the dependencies cache b.crun_cache_keys = v_program_files b.crun_cache_keys << exe_name // just check the timestamps for now: exe_stamp := os.file_last_mod_unix(exe_name) source_stamp := most_recent_timestamp(v_program_files) if exe_stamp <= source_stamp { return true } //////////////////////////////////////////////////////////////////////////// // The timestamps for the top level files were found ok, // however we want to *also* make sure that a full rebuild will be done // if any of the dependencies (if we know them) are changed. 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.crun_build_options_signature() { return true } sdependencies := cm.load('.dependencies', '.crun') or { // empty/wiped out cache, we do not know what the dependencies are, so just // rebuild, which will fill in the dependencies cache for the next crun return true } 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 false } fn most_recent_timestamp(files []string) i64 { mut res := i64(0) for f in files { f_stamp := os.file_last_mod_unix(f) if res <= f_stamp { res = f_stamp } } return res } pub fn (mut b Builder) rebuild(backend_cb FnBackend) { mut sw := time.new_stopwatch() backend_cb(mut b) 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.crun_dependency_files() cm.save('.dependencies', '.crun', dependency_files.join('\n')) or {} cm.save('.build_options', '.crun', b.crun_build_options_signature()) or {} } mut timers := util.get_timers() timers.show_remaining() if b.pref.is_stats { compilation_time_micros := 1 + sw.elapsed().microseconds() scompilation_time_ms := util.bold('${f64(compilation_time_micros) / 1000.0:6.3f}') mut all_v_source_lines, mut all_v_source_bytes, mut all_v_source_tokens := 0, 0, 0 mut all_v_top_stmts, mut all_non_vlib_top_stmts, mut all_main_top_stmts := 0, 0, 0 for pf in b.parsed_files { all_v_source_lines += pf.nr_lines all_v_source_bytes += pf.nr_bytes all_v_source_tokens += pf.nr_tokens all_v_top_stmts += pf.stmts.len if !pf.path.contains('vlib/') { all_non_vlib_top_stmts += pf.stmts.len } if pf.mod.name == 'main' { all_main_top_stmts += pf.stmts.len } } mut sall_top_stmts := all_v_top_stmts.str() mut sall_non_vlib_top_stmts := all_non_vlib_top_stmts.str() mut sall_main_top_stmts := all_main_top_stmts.str() mut sall_v_source_lines := all_v_source_lines.str() mut sall_v_source_bytes := all_v_source_bytes.str() mut sall_v_source_tokens := all_v_source_tokens.str() mut sall_v_types := b.table.type_symbols.len.str() mut sall_v_modules := b.table.modules.len.str() mut sall_v_files := b.parsed_files.len.str() sall_v_source_lines = util.bold('${sall_v_source_lines:10s}') sall_v_source_bytes = util.bold('${sall_v_source_bytes:10s}') sall_v_source_tokens = util.bold('${sall_v_source_tokens:10s}') sall_v_types = util.bold('${sall_v_types:5s}') sall_v_modules = util.bold('${sall_v_modules:5s}') sall_v_files = util.bold('${sall_v_files:5s}') sall_top_stmts = util.bold('${sall_top_stmts:5s}') sall_non_vlib_top_stmts = util.bold('${sall_non_vlib_top_stmts:5s}') sall_main_top_stmts = util.bold('${sall_main_top_stmts:5s}') println(' V source code size: ${sall_v_source_lines} lines, ${sall_v_source_tokens} tokens, ${sall_v_source_bytes} bytes, ${sall_v_types} types, ${sall_v_modules} modules, ${sall_v_files} files, ${sall_top_stmts} tl_stmts, ${sall_non_vlib_top_stmts} non_vlib_tl_stmts, ${sall_main_top_stmts} main_tl_stmts') // mut slines := b.stats_lines.str() mut sbytes := b.stats_bytes.str() slines = util.bold('${slines:10s}') sbytes = util.bold('${sbytes:10s}') println('generated target code size: ${slines} lines, ${sbytes} bytes') // vlines_per_second := int(1_000_000.0 * f64(all_v_source_lines) / f64(compilation_time_micros)) svlines_per_second := util.bold(vlines_per_second.str()) used_cgen_threads := if b.pref.no_parallel { 1 } else { runtime.nr_jobs() } println('compilation took: ${scompilation_time_ms} ms, compilation speed: ${svlines_per_second} vlines/s, cgen threads: ${used_cgen_threads}') } } 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 := '' if !b.pref.reuse_tmpc { uniq = '.${rand.ulid()}' } fname := sanitized_vtmp_basename(base_file_name) + '${uniq}${postfix}' return os.real_path(os.join_path(vtmp, fname)) } fn sanitized_vtmp_basename(base_file_name string) string { name := os.file_name(os.real_path(base_file_name)) mut sanitized := strings.new_builder(name.len) for ch in name { if ch >= 128 || (ch >= `0` && ch <= `9`) || (ch >= `A` && ch <= `Z`) || (ch >= `a` && ch <= `z`) || ch in [`-`, `.`, `_`] { sanitized.write_u8(ch) } else { sanitized.write_u8(`_`) } } result := sanitized.str() if result in ['', '.', '..'] { return 'vtmp' } return result }