v2 / vlib / v / builder / rebuilding.v
512 lines · 492 sloc · 15.83 KB · 27a427d17fc35ea774f2d881efee4686f2d0180d
Raw
1module builder
2
3import os
4import hash
5import time
6import rand
7import strings
8import v.ast
9import v.util
10import v.pref
11import v.vcache
12import runtime
13
14const crun_cache_format_version = 'crun_cache_v2'
15
16pub fn (mut b Builder) rebuild_modules() {
17 if !b.pref.use_cache || b.pref.build_mode == .build_module {
18 return
19 }
20 if b.pref.check_only || b.pref.only_check_syntax {
21 // Check-only flows should not trigger side-effecting cache rebuilds.
22 // In the REPL import path that can compile imported `.c.v` modules
23 // and surface irrelevant missing-header errors for declaration-only checks.
24 return
25 }
26 all_files := b.parsed_files.map(it.path)
27 $if trace_invalidations ? {
28 eprintln('> rebuild_modules all_files: ${all_files}')
29 }
30 invalidations := b.find_invalidated_modules_by_files(all_files)
31 $if trace_invalidations ? {
32 eprintln('> rebuild_modules invalidations: ${invalidations}')
33 }
34 if invalidations.len > 0 {
35 vexe := pref.vexe_path()
36 for imp in invalidations {
37 b.v_build_module(vexe, imp)
38 }
39 }
40}
41
42pub fn (mut b Builder) find_invalidated_modules_by_files(all_files []string) []string {
43 util.timing_start('${@METHOD} source_hashing')
44 mut new_hashes := map[string]string{}
45 mut old_hashes := map[string]string{}
46 mut sb_new_hashes := strings.new_builder(1024)
47
48 mut cm := vcache.new_cache_manager(all_files)
49 sold_hashes := cm.load('.hashes', 'all_files') or { ' ' }
50 // eprintln(sold_hashes)
51 sold_hashes_lines := sold_hashes.split('\n')
52 for line in sold_hashes_lines {
53 if line.len == 0 {
54 continue
55 }
56 x := line.split(' ')
57 chash := x[0]
58 cpath := x[1]
59 old_hashes[cpath] = chash
60 }
61 // eprintln('old_hashes: ${old_hashes}')
62 for cpath in all_files {
63 ccontent := util.read_file(cpath) or { '' }
64 chash := hash.sum64_string(ccontent, 7).hex_full()
65 new_hashes[cpath] = chash
66 sb_new_hashes.write_string(chash)
67 sb_new_hashes.write_u8(` `)
68 sb_new_hashes.write_string(cpath)
69 sb_new_hashes.write_u8(`\n`)
70 }
71 snew_hashes := sb_new_hashes.str()
72 // eprintln('new_hashes: ${new_hashes}')
73 // eprintln('> new_hashes != old_hashes: ' + ( old_hashes != new_hashes ).str())
74 // eprintln(snew_hashes)
75 cm.save('.hashes', 'all_files', snew_hashes) or {}
76 util.timing_measure('${@METHOD} source_hashing')
77
78 mut invalidations := []string{}
79 if new_hashes != old_hashes {
80 util.timing_start('${@METHOD} rebuilding')
81 // eprintln('> b.mod_invalidates_paths: ${b.mod_invalidates_paths}')
82 // eprintln('> b.mod_invalidates_mods: ${b.mod_invalidates_mods}')
83 // eprintln('> b.path_invalidates_mods: ${b.path_invalidates_mods}')
84 $if trace_invalidations ? {
85 for k, v in b.mod_invalidates_paths {
86 mut m := map[string]bool{}
87 for mm in b.mod_invalidates_mods[k] {
88 m[mm] = true
89 }
90 eprintln('> module `${k}` invalidates: ${m.keys()}')
91 for fpath in v {
92 eprintln(' ${fpath}')
93 }
94 }
95 }
96 mut invalidated_paths := map[string]int{}
97 mut invalidated_mod_paths := map[string]int{}
98 for npath, nhash in new_hashes {
99 if npath !in old_hashes {
100 invalidated_paths[npath]++
101 continue
102 }
103 if old_hashes[npath] != nhash {
104 invalidated_paths[npath]++
105 continue
106 }
107 }
108 for opath, ohash in old_hashes {
109 if opath !in new_hashes {
110 invalidated_paths[opath]++
111 continue
112 }
113 if new_hashes[opath] != ohash {
114 invalidated_paths[opath]++
115 continue
116 }
117 }
118 $if trace_invalidations ? {
119 eprintln('invalidated_paths: ${invalidated_paths}')
120 }
121 mut rebuild_everything := false
122 for cycle := 0; true; cycle++ {
123 $if trace_invalidations ? {
124 eprintln('> cycle: ${cycle} | invalidated_paths: ${invalidated_paths}')
125 }
126 mut new_invalidated_paths := map[string]int{}
127 for npath, _ in invalidated_paths {
128 invalidated_mods := b.path_invalidates_mods[npath]
129 if invalidated_mods == ['main'] {
130 continue
131 }
132 if 'builtin' in invalidated_mods {
133 // When `builtin` is invalid, there is no point in
134 // extracting a finer grained dependency resolution
135 // of the dependencies any more. Instead, just rebuild
136 // every module.
137 rebuild_everything = true
138 break
139 }
140 for imod in invalidated_mods {
141 if imod == 'main' {
142 continue
143 }
144 for np in b.mod_invalidates_paths[imod] {
145 new_invalidated_paths[np]++
146 }
147 }
148 $if trace_invalidations ? {
149 eprintln('> npath -> invalidated_mods | ${npath} -> ${invalidated_mods}')
150 }
151 mpath := os.dir(npath)
152 invalidated_mod_paths[mpath]++
153 }
154 if rebuild_everything {
155 break
156 }
157 if new_invalidated_paths.len == 0 {
158 break
159 }
160 invalidated_paths = new_invalidated_paths.clone()
161 }
162 if rebuild_everything {
163 invalidated_mod_paths = {}
164 for npath, _ in new_hashes {
165 mpath := os.dir(npath)
166 pimods := b.path_invalidates_mods[npath]
167 if pimods == ['main'] {
168 continue
169 }
170 invalidated_mod_paths[mpath]++
171 }
172 }
173 $if trace_invalidations ? {
174 eprintln('invalidated_mod_paths: ${invalidated_mod_paths}')
175 eprintln('rebuild_everything: ${rebuild_everything}')
176 }
177 if invalidated_mod_paths.len > 0 {
178 impaths := invalidated_mod_paths.keys()
179 for imp in impaths {
180 invalidations << imp
181 }
182 }
183 util.timing_measure('${@METHOD} rebuilding')
184 }
185 return invalidations
186}
187
188fn (mut b Builder) v_build_module(vexe string, imp_path string) {
189 pwd := os.getwd()
190 defer {
191 os.chdir(pwd) or {}
192 }
193 // do run `v build-module x` always in main vfolder; x can be a relative path
194 vroot := os.dir(vexe)
195 os.chdir(vroot) or {}
196 boptions := b.pref.build_options.join(' ')
197 rebuild_cmd := '${os.quoted_path(vexe)} ${boptions} build-module ${os.quoted_path(imp_path)}'
198 vcache.dlog('| Builder.' + @FN,
199 'vexe: ${vexe} | imp_path: ${imp_path} | rebuild_cmd: ${rebuild_cmd}')
200 $if trace_v_build_module ? {
201 eprintln('> Builder.v_build_module: ${rebuild_cmd}')
202 }
203 // eprintln('> Builder.v_build_module: ${rebuild_cmd}')
204 os.system(rebuild_cmd)
205}
206
207fn (mut b Builder) rebuild_cached_module(vexe string, imp_path string) string {
208 res := b.pref.cache_manager.mod_exists(imp_path, '.o', imp_path) or {
209 if b.pref.is_verbose {
210 println('Cached ${imp_path} .o file not found... Building .o file for ${imp_path}')
211 }
212 b.v_build_module(vexe, imp_path)
213 rebuilt_o := b.pref.cache_manager.mod_exists(imp_path, '.o', imp_path) or {
214 panic('could not rebuild cache module for ${imp_path}, error: ${err.msg()}')
215 }
216 return rebuilt_o
217 }
218 return res
219}
220
221fn (mut b Builder) handle_usecache(vexe string) {
222 if !b.pref.use_cache || b.pref.build_mode == .build_module {
223 return
224 }
225 mut libs := []string{} // builtin.o os.o http.o etc
226 mut built_modules := []string{}
227 builtin_obj_path := b.rebuild_cached_module(vexe, 'vlib/builtin')
228 libs << builtin_obj_path
229 for ast_file in b.parsed_files {
230 if b.pref.is_test && ast_file.mod.name != 'main' {
231 imp_path := b.find_module_path(ast_file.mod.name, ast_file.path) or {
232 verror('cannot import module "${ast_file.mod.name}" (not found)')
233 break
234 }
235 obj_path := b.rebuild_cached_module(vexe, imp_path)
236 libs << obj_path
237 built_modules << ast_file.mod.name
238 }
239 for imp_stmt in ast_file.imports {
240 imp := imp_stmt.mod
241 // strconv is already imported inside builtin, so skip generating its object file
242 // TODO: in case we have other modules with the same name, make sure they are vlib
243 // is this even doing anything?
244 if util.module_is_builtin(imp) {
245 continue
246 }
247 if imp in built_modules {
248 continue
249 }
250 if util.should_bundle_module(imp) {
251 continue
252 }
253 // The problem is cmd/v is in module main and imports
254 // the relative module named help, which is built as cmd.v.help not help
255 // currently this got this working by building into main, see ast.FnDecl in cgen
256 if imp == 'help' {
257 continue
258 }
259 imp_path := b.find_module_path(imp, ast_file.path) or {
260 verror('cannot import module "${imp}" (not found)')
261 break
262 }
263 obj_path := b.rebuild_cached_module(vexe, imp_path)
264 libs << obj_path
265 built_modules << imp
266 }
267 }
268 b.ccoptions.post_args << libs
269}
270
271pub fn (mut b Builder) should_rebuild() bool {
272 mut exe_name := b.pref.out_name
273 $if windows {
274 exe_name += '.exe'
275 }
276 if !os.is_file(exe_name) {
277 return true
278 }
279 if !b.pref.is_crun {
280 return true
281 }
282 mut v_program_files := []string{}
283 is_file := os.is_file(b.pref.path)
284 is_dir := os.is_dir(b.pref.path)
285 if is_file {
286 v_program_files << b.pref.path
287 } else if is_dir {
288 v_program_files << b.v_files_from_dir(b.pref.path)
289 }
290 v_program_files.sort() // ensure stable keys for the dependencies cache
291 b.crun_cache_keys = v_program_files
292 b.crun_cache_keys << exe_name
293 // just check the timestamps for now:
294 exe_stamp := os.file_last_mod_unix(exe_name)
295 source_stamp := most_recent_timestamp(v_program_files)
296 if exe_stamp <= source_stamp {
297 return true
298 }
299 ////////////////////////////////////////////////////////////////////////////
300 // The timestamps for the top level files were found ok,
301 // however we want to *also* make sure that a full rebuild will be done
302 // if any of the dependencies (if we know them) are changed.
303 mut cm := vcache.new_cache_manager(b.crun_cache_keys)
304 // always rebuild, when the compilation options changed between 2 sequential cruns:
305 sbuild_options := cm.load('.build_options', '.crun') or { return true }
306 if sbuild_options != b.crun_build_options_signature() {
307 return true
308 }
309 sdependencies := cm.load('.dependencies', '.crun') or {
310 // empty/wiped out cache, we do not know what the dependencies are, so just
311 // rebuild, which will fill in the dependencies cache for the next crun
312 return true
313 }
314 dependencies := sdependencies.split('\n').filter(it != '')
315 for dependency in dependencies {
316 if !os.is_file(dependency) {
317 return true
318 }
319 if os.file_last_mod_unix(dependency) >= exe_stamp {
320 return true
321 }
322 }
323 return false
324}
325
326fn most_recent_timestamp(files []string) i64 {
327 mut res := i64(0)
328 for f in files {
329 f_stamp := os.file_last_mod_unix(f)
330 if res <= f_stamp {
331 res = f_stamp
332 }
333 }
334 return res
335}
336
337pub fn (mut b Builder) rebuild(backend_cb FnBackend) {
338 mut sw := time.new_stopwatch()
339 backend_cb(mut b)
340 if b.pref.is_crun {
341 // save the dependencies after the first compilation, they will be used for subsequent ones:
342 mut cm := vcache.new_cache_manager(b.crun_cache_keys)
343 dependency_files := b.crun_dependency_files()
344 cm.save('.dependencies', '.crun', dependency_files.join('\n')) or {}
345 cm.save('.build_options', '.crun', b.crun_build_options_signature()) or {}
346 }
347 mut timers := util.get_timers()
348 timers.show_remaining()
349 if b.pref.is_stats {
350 compilation_time_micros := 1 + sw.elapsed().microseconds()
351 scompilation_time_ms := util.bold('${f64(compilation_time_micros) / 1000.0:6.3f}')
352 mut all_v_source_lines, mut all_v_source_bytes, mut all_v_source_tokens := 0, 0, 0
353 mut all_v_top_stmts, mut all_non_vlib_top_stmts, mut all_main_top_stmts := 0, 0, 0
354 for pf in b.parsed_files {
355 all_v_source_lines += pf.nr_lines
356 all_v_source_bytes += pf.nr_bytes
357 all_v_source_tokens += pf.nr_tokens
358 all_v_top_stmts += pf.stmts.len
359 if !pf.path.contains('vlib/') {
360 all_non_vlib_top_stmts += pf.stmts.len
361 }
362 if pf.mod.name == 'main' {
363 all_main_top_stmts += pf.stmts.len
364 }
365 }
366 mut sall_top_stmts := all_v_top_stmts.str()
367 mut sall_non_vlib_top_stmts := all_non_vlib_top_stmts.str()
368 mut sall_main_top_stmts := all_main_top_stmts.str()
369 mut sall_v_source_lines := all_v_source_lines.str()
370 mut sall_v_source_bytes := all_v_source_bytes.str()
371 mut sall_v_source_tokens := all_v_source_tokens.str()
372 mut sall_v_types := b.table.type_symbols.len.str()
373 mut sall_v_modules := b.table.modules.len.str()
374 mut sall_v_files := b.parsed_files.len.str()
375 sall_v_source_lines = util.bold('${sall_v_source_lines:10s}')
376 sall_v_source_bytes = util.bold('${sall_v_source_bytes:10s}')
377 sall_v_source_tokens = util.bold('${sall_v_source_tokens:10s}')
378 sall_v_types = util.bold('${sall_v_types:5s}')
379 sall_v_modules = util.bold('${sall_v_modules:5s}')
380 sall_v_files = util.bold('${sall_v_files:5s}')
381 sall_top_stmts = util.bold('${sall_top_stmts:5s}')
382 sall_non_vlib_top_stmts = util.bold('${sall_non_vlib_top_stmts:5s}')
383 sall_main_top_stmts = util.bold('${sall_main_top_stmts:5s}')
384 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')
385 //
386 mut slines := b.stats_lines.str()
387 mut sbytes := b.stats_bytes.str()
388 slines = util.bold('${slines:10s}')
389 sbytes = util.bold('${sbytes:10s}')
390 println('generated target code size: ${slines} lines, ${sbytes} bytes')
391 //
392 vlines_per_second := int(1_000_000.0 * f64(all_v_source_lines) / f64(compilation_time_micros))
393 svlines_per_second := util.bold(vlines_per_second.str())
394 used_cgen_threads := if b.pref.no_parallel { 1 } else { runtime.nr_jobs() }
395 println('compilation took: ${scompilation_time_ms} ms, compilation speed: ${svlines_per_second} vlines/s, cgen threads: ${used_cgen_threads}')
396 }
397}
398
399fn (b &Builder) crun_build_options_signature() string {
400 mut parts := []string{cap: b.pref.build_options.len + 1}
401 parts << crun_cache_format_version
402 parts << b.pref.build_options
403 return parts.join('\n')
404}
405
406fn add_existing_crun_dependency(mut dependencies map[string]bool, path string) {
407 if path == '' {
408 return
409 }
410 real_path := os.real_path(path)
411 if os.is_file(real_path) {
412 dependencies[real_path] = true
413 }
414}
415
416fn (b &Builder) crun_hash_stmt_dependency_path(node ast.HashStmt) string {
417 match node.kind {
418 'include', 'preinclude', 'postinclude' {
419 if node.main.starts_with('<') && node.main.ends_with('>') {
420 return ''
421 }
422 mut path := node.main.trim('"')
423 if !os.is_abs_path(path) {
424 path = os.join_path(os.dir(node.source_file), path)
425 }
426 return path
427 }
428 'insert' {
429 mut path := node.main.trim('"')
430 if !os.is_abs_path(path) {
431 path = os.join_path(os.dir(node.source_file), path)
432 }
433 return path
434 }
435 else {
436 return ''
437 }
438 }
439}
440
441fn (b &Builder) collect_crun_stmt_dependencies(mut dependencies map[string]bool, stmt ast.Stmt) {
442 match stmt {
443 ast.HashStmt {
444 add_existing_crun_dependency(mut dependencies, b.crun_hash_stmt_dependency_path(stmt))
445 }
446 ast.ExprStmt {
447 if stmt.expr is ast.IfExpr && stmt.expr.is_comptime {
448 b.collect_crun_if_expr_dependencies(mut dependencies, stmt.expr)
449 }
450 }
451 else {}
452 }
453}
454
455fn (b &Builder) collect_crun_if_expr_dependencies(mut dependencies map[string]bool, expr ast.IfExpr) {
456 for branch in expr.branches {
457 for stmt in branch.stmts {
458 b.collect_crun_stmt_dependencies(mut dependencies, stmt)
459 }
460 }
461}
462
463fn (mut b Builder) crun_dependency_files() []string {
464 mut dependencies := map[string]bool{}
465 for file in b.parsed_files {
466 add_existing_crun_dependency(mut dependencies, file.path)
467 for template_path in file.template_paths {
468 add_existing_crun_dependency(mut dependencies, template_path)
469 }
470 for embedded_file in file.embedded_files {
471 add_existing_crun_dependency(mut dependencies, embedded_file.apath)
472 }
473 for stmt in file.stmts {
474 b.collect_crun_stmt_dependencies(mut dependencies, stmt)
475 }
476 }
477 for cflag in b.get_os_cflags() {
478 value := cflag.eval() or { continue }
479 add_existing_crun_dependency(mut dependencies, value)
480 }
481 mut files := dependencies.keys()
482 files.sort()
483 return files
484}
485
486pub fn (mut b Builder) get_vtmp_filename(base_file_name string, postfix string) string {
487 vtmp := os.vtmp_dir()
488 mut uniq := ''
489 if !b.pref.reuse_tmpc {
490 uniq = '.${rand.ulid()}'
491 }
492 fname := sanitized_vtmp_basename(base_file_name) + '${uniq}${postfix}'
493 return os.real_path(os.join_path(vtmp, fname))
494}
495
496fn sanitized_vtmp_basename(base_file_name string) string {
497 name := os.file_name(os.real_path(base_file_name))
498 mut sanitized := strings.new_builder(name.len)
499 for ch in name {
500 if ch >= 128 || (ch >= `0` && ch <= `9`) || (ch >= `A` && ch <= `Z`)
501 || (ch >= `a` && ch <= `z`) || ch in [`-`, `.`, `_`] {
502 sanitized.write_u8(ch)
503 } else {
504 sanitized.write_u8(`_`)
505 }
506 }
507 result := sanitized.str()
508 if result in ['', '.', '..'] {
509 return 'vtmp'
510 }
511 return result
512}
513