vq / vlib / v / builder / compile.v
470 lines · 443 sloc · 14.77 KB · 712a7493d966be2e24bcf74ccd45e6adf14d64bf
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module builder
5
6import os
7import v.pref
8import v.util
9import v.vmod
10
11pub type FnBackend = fn (mut b Builder)
12
13// should_find_windows_host_c_compiler reports whether Windows host compiler probing is needed.
14pub fn should_find_windows_host_c_compiler(pref_ &pref.Preferences) bool {
15 return pref_.backend == .c && pref_.os == .windows && !pref_.output_cross_c
16}
17
18pub fn compile(command string, pref_ &pref.Preferences, backend_cb FnBackend) {
19 if pref_.is_test {
20 disable_c_error_bug_reports()
21 }
22 check_if_input_file_exists(pref_)
23 check_if_output_folder_is_writable(pref_)
24 $if windows {
25 if should_find_windows_host_c_compiler(pref_) {
26 // Resolve the effective Windows C compiler before builder initialization.
27 mut probe := Builder{
28 pref: unsafe { pref_ }
29 }
30 probe.find_win_cc() or {}
31 }
32 }
33 mut pref_ref := unsafe { pref_ }
34 pref_ref.ccompiler_type = resolve_ccompiler_type(pref_ref.ccompiler, pref_ref.ccompiler_type)
35 // Construct the V object from command line arguments
36 mut b := new_builder(pref_)
37 if b.should_rebuild() {
38 b.rebuild(backend_cb)
39 }
40 b.exit_on_invalid_syntax()
41 // running does not require the parsers anymore
42 unsafe { b.myfree() }
43 b.run_compiled_executable_and_exit()
44}
45
46fn check_if_input_file_exists(pref_ &pref.Preferences) {
47 if pref_.path == '' || pref_.path == '-' {
48 return
49 }
50 if pref_.path.ends_with('.v') || pref_.path.ends_with('.vsh') || pref_.path.ends_with('.vv') {
51 if !os.exists(pref_.path) {
52 verror("${pref_.path} doesn't exist")
53 }
54 }
55}
56
57fn check_if_output_folder_is_writable(pref_ &pref.Preferences) {
58 if pref_.should_output_to_stdout() || pref_.check_only || pref_.only_check_syntax
59 || pref_.backend == .interpret {
60 return
61 }
62 odir := os.dir(pref_.out_name)
63 // When pref.out_name is just the name of an executable, i.e. `./v -o executable main.v`
64 // without a folder component, just use the current folder instead:
65 mut output_folder := odir
66 if odir.len == pref_.out_name.len {
67 output_folder = os.getwd()
68 }
69 os.mkdir_all(output_folder) or { verror(err.msg()) }
70 os.ensure_folder_is_writable(output_folder) or {
71 // An early error here, is better than an unclear C error later:
72 verror(err.msg())
73 }
74}
75
76// Temporary, will be done by -autofree
77@[unsafe]
78fn (mut b Builder) myfree() {
79 // for file in b.parsed_files {
80 // }
81 unsafe { b.parsed_files.free() }
82 util.free_caches()
83}
84
85fn (b &Builder) exit_on_invalid_syntax() {
86 util.free_caches()
87 // V should exit with an exit code of 1, when there are errors,
88 // even when -silent is passed in combination to -check-syntax:
89 if b.pref.only_check_syntax {
90 for pf in b.parsed_files {
91 if pf.errors.len > 0 {
92 exit(1)
93 }
94 }
95 if b.checker.nr_errors > 0 {
96 exit(1)
97 }
98 }
99}
100
101fn (mut b Builder) run_compiled_executable_and_exit() {
102 if b.pref.backend == .interpret {
103 // the interpreted code has already ran
104 return
105 }
106 if b.pref.skip_running {
107 return
108 }
109 if b.pref.only_check_syntax || b.pref.check_only {
110 return
111 }
112 if b.pref.should_output_to_stdout() {
113 return
114 }
115 if b.pref.os == .ios {
116 panic('Running iOS apps is not supported yet.')
117 }
118 if !(b.pref.is_test || b.pref.is_run || b.pref.is_crun) {
119 exit(0)
120 }
121 mut compiled_file := b.pref.out_name
122 if b.pref.backend == .wasm && !compiled_file.ends_with('.wasm') {
123 compiled_file += '.wasm'
124 }
125 if b.pref.backend == .c && b.pref.os == .windows && !compiled_file.ends_with('.exe') {
126 compiled_file += '.exe'
127 }
128 compiled_file = os.real_path(compiled_file)
129
130 mut run_args := []string{cap: b.pref.run_args.len + 1}
131
132 run_file := if b.pref.backend.is_js() {
133 node_basename := $if windows { 'node.exe' } $else { 'node' }
134 os.find_abs_path_of_executable(node_basename) or {
135 panic('Could not find `${node_basename}` in system path. Do you have Node.js installed?')
136 }
137 } else if b.pref.backend == .wasm {
138 mut actual_run := ['wasmer', 'wasmtime', 'wavm', 'wasm3', 'iwasm']
139 mut actual_rf := ''
140
141 // -autofree bug
142 // error: cannot convert 'struct string' to 'struct _option_string'
143 // mut actual_rf := ?string(none)
144
145 for runtime in actual_run {
146 basename := $if windows { runtime + '.exe' } $else { runtime }
147
148 if rf := os.find_abs_path_of_executable(basename) {
149 if basename == 'wavm' {
150 run_args << 'run'
151 }
152 actual_rf = rf
153 break
154 }
155 }
156
157 if actual_rf == '' {
158 panic('Could not find `wasmer`, `wasmtime`, `wavm`, `wasm3` or `ìwasm` in system path. Do you have any installed?')
159 }
160
161 actual_rf
162 } else {
163 compiled_file
164 }
165 if b.pref.backend.is_js() || b.pref.backend == .wasm {
166 run_args << compiled_file
167 }
168 run_args << b.pref.run_args
169
170 if b.pref.is_verbose {
171 println('running ${run_file} with arguments ${run_args.join(' ')}')
172 }
173 if b.pref.is_shared {
174 verror('can not run shared library ${run_file}')
175 }
176 mut ret := 0
177 if b.pref.use_os_system_to_run {
178 mut command_to_run := os.quoted_path(run_file)
179 if run_args.len > 0 {
180 command_to_run += ' ' + util.args_quote_paths(run_args)
181 }
182 ret = os.system(command_to_run)
183 // eprintln('> ret: ${ret:5} | command_to_run: ${command_to_run}')
184 } else {
185 mut run_process := os.new_process(run_file)
186 run_process.set_args(run_args)
187 // Ignore sigint and sigquit while running the compiled file,
188 // so ^C doesn't prevent v from deleting the compiled file.
189 // See also https://git.musl-libc.org/cgit/musl/tree/src/process/system.c
190 prev_int_handler := os.signal_opt(.int, eshcb) or { serror('set .int', err) }
191 mut prev_quit_handler := os.SignalHandler(eshcb)
192 $if !windows { // There's no sigquit on windows
193 prev_quit_handler = os.signal_opt(.quit, eshcb) or { serror('set .quit', err) }
194 }
195 run_process.wait()
196 os.signal_opt(.int, prev_int_handler) or { serror('restore .int', err) }
197 $if !windows {
198 os.signal_opt(.quit, prev_quit_handler) or { serror('restore .quit', err) }
199 }
200 ret = run_process.code
201 if run_process.err != '' {
202 eprintln(run_process.err)
203 }
204 run_process.close()
205 }
206 b.cleanup_run_executable_after_exit(compiled_file)
207 exit(ret)
208}
209
210fn eshcb(_ os.Signal) {
211}
212
213@[noreturn]
214fn serror(reason string, e IError) {
215 eprintln('could not ${reason} handler')
216 panic(e.msg())
217}
218
219fn (mut v Builder) cleanup_run_executable_after_exit(exefile string) {
220 if v.pref.is_crun {
221 return
222 }
223 if v.pref.reuse_tmpc {
224 v.pref.vrun_elog('keeping executable: ${exefile} , because -keepc was passed')
225 return
226 }
227 if !v.executable_exists {
228 v.pref.vrun_elog('remove run executable: ${exefile}')
229 os.rm(exefile) or {}
230 }
231}
232
233// 'strings' => 'VROOT/vlib/strings'
234// 'installed_mod' => '~/.vmodules/installed_mod'
235// 'local_mod' => '/path/to/current/dir/local_mod'
236pub fn (mut v Builder) set_module_lookup_paths() {
237 // Module search order:
238 // 0) V test files are very commonly located right inside the folder of the
239 // module, which they test. Adding the parent folder of the module folder
240 // with the _test.v files, *guarantees* that the tested module can be found
241 // without needing to set custom options/flags.
242 // 1) search in the *same* directory, as the compiled final v program source
243 // (i.e. the . in `v .` or file.v in `v file.v`)
244 // 2) search in the modules/ in the same directory.
245 // 3) search in the provided paths
246 // By default, these are what (3) contains:
247 // 3.1) search in vlib/
248 // 3.2) search in ~/.vmodules/ (i.e. modules installed with vpm)
249 lookup_root := v.module_lookup_root()
250 v.module_search_paths = []
251 if v.pref.is_test {
252 v.module_search_paths << os.dir(v.compiled_dir) // pdir of _test.v
253 }
254 v.module_search_paths << lookup_root
255 x := os.join_path(lookup_root, 'modules')
256 if v.pref.is_verbose {
257 println('x: "${x}"')
258 }
259
260 if source_root := source_root_from_vmod_root(lookup_root) {
261 source_modules := os.join_path(source_root, 'modules')
262 if source_modules !in v.module_search_paths && os.exists(source_modules) {
263 v.module_search_paths << source_modules
264 }
265 }
266 if os.exists(os.join_path(lookup_root, 'modules')) {
267 v.module_search_paths << os.join_path(lookup_root, 'modules')
268 }
269
270 v.module_search_paths << v.pref.lookup_path
271 if v.pref.is_verbose {
272 v.log('v.module_search_paths:')
273 println(v.module_search_paths)
274 }
275}
276
277fn (v &Builder) module_lookup_root() string {
278 // If `compiled_dir` is the base_url-configured source folder, treat the
279 // enclosing module folder as the lookup root so sibling `modules/` resolves.
280 mut mcache := vmod.get_cache()
281 vmod_file_location := mcache.get_by_folder(v.compiled_dir)
282 if vmod_file_location.vmod_file != '' && vmod_file_location.vmod_folder != v.compiled_dir {
283 if source_root := source_root_from_vmod_root(vmod_file_location.vmod_folder) {
284 if os.real_path(source_root) == v.compiled_dir {
285 return vmod_file_location.vmod_folder
286 }
287 }
288 }
289 return v.compiled_dir
290}
291
292pub fn (v Builder) get_builtin_files() []string {
293 if v.pref.no_builtin {
294 v.log('v.pref.no_builtin is true, get_builtin_files == []')
295 return []
296 }
297 v.log('v.pref.lookup_path: ${v.pref.lookup_path}')
298 // Lookup for built-in folder in lookup path.
299 // Assumption: `builtin/` folder implies usable implementation of builtin
300 for location in v.pref.lookup_path {
301 if os.exists(os.join_path(location, 'builtin')) {
302 mut builtin_files := []string{}
303 if v.pref.backend.is_js() {
304 builtin_files << v.v_files_from_dir(os.join_path(location, 'builtin', 'js'))
305 } else if v.pref.backend == .wasm {
306 builtin_files << v.v_files_from_dir(os.join_path(location, 'builtin', 'wasm'))
307 if v.pref.os == .browser {
308 builtin_files << v.v_files_from_dir(os.join_path(location, 'builtin', 'wasm',
309 'browser'))
310 } else {
311 builtin_files << v.v_files_from_dir(os.join_path(location, 'builtin', 'wasm',
312 'wasi'))
313 }
314 } else {
315 builtin_files << v.v_files_from_dir(os.join_path(location, 'builtin'))
316 }
317 if v.pref.is_bare {
318 builtin_files << v.v_files_from_dir(v.pref.bare_builtin_dir)
319 }
320 if v.pref.backend == .c {
321 // TODO: JavaScript backend doesn't handle os for now
322 if v.pref.is_vsh && os.exists(os.join_path(location, 'os')) {
323 builtin_files << v.v_files_from_dir(os.join_path(location, 'os'))
324 }
325 }
326 return builtin_files
327 }
328 }
329 // Panic. We couldn't find the folder.
330 verror('`builtin/` not included on module lookup path.\nDid you forget to add vlib to the path? (Use @vlib for default vlib)')
331}
332
333pub fn (v &Builder) get_user_files() []string {
334 if v.pref.path in ['vlib/builtin', 'vlib/strconv', 'vlib/strings', 'vlib/hash']
335 || v.pref.path.ends_with('vlib/builtin') {
336 // This means we are building a builtin module with `v build-module vlib/strings` etc
337 // get_builtin_files() has already added the files in this module,
338 // do nothing here to avoid duplicate definition errors.
339 v.log('Skipping user files.')
340 return []
341 }
342 mut dir := v.pref.path
343 v.log('get_v_files(${dir})')
344 // Need to store user files separately, because they have to be added after
345 // libs, but we dont know which libs need to be added yet
346 mut user_files := []string{}
347 // See cmd/tools/preludes/README.md for more info about what preludes are
348 mut preludes_path := os.join_path(v.pref.vroot, 'vlib', 'v', 'preludes')
349 if v.pref.backend == .js_node {
350 preludes_path = os.join_path(v.pref.vroot, 'vlib', 'v', 'preludes_js')
351 }
352 if v.pref.trace_calls {
353 user_files << os.join_path(preludes_path, 'trace_calls.v')
354 }
355 if v.pref.is_livemain || v.pref.is_liveshared {
356 user_files << os.join_path(preludes_path, 'live.v')
357 }
358 if v.pref.is_livemain {
359 user_files << os.join_path(preludes_path, 'live_main.v')
360 }
361 if v.pref.is_liveshared {
362 user_files << os.join_path(preludes_path, 'live_shared.v')
363 }
364 if v.pref.is_test {
365 if v.pref.backend == .js_node {
366 user_files << os.join_path(preludes_path, 'test_runner.v')
367 } else {
368 user_files << os.join_path(preludes_path, 'test_runner.c.v')
369 }
370 //
371 mut v_test_runner_prelude := os.getenv('VTEST_RUNNER')
372 if v.pref.test_runner != '' {
373 v_test_runner_prelude = v.pref.test_runner
374 }
375 if v_test_runner_prelude == '' {
376 v_test_runner_prelude = 'normal'
377 }
378 if !v_test_runner_prelude.contains('/') && !v_test_runner_prelude.contains('\\')
379 && !v_test_runner_prelude.ends_with('.v') {
380 v_test_runner_prelude = os.join_path(preludes_path,
381 'test_runner_${v_test_runner_prelude}.v')
382 }
383 if !os.is_file(v_test_runner_prelude) || !os.is_readable(v_test_runner_prelude) {
384 eprintln('test runner error: File ${v_test_runner_prelude} should be readable.')
385 verror('the supported test runners are: ${pref.supported_test_runners_list()}')
386 }
387 user_files << v_test_runner_prelude
388 }
389 if v.pref.is_test && v.pref.show_asserts {
390 user_files << os.join_path(preludes_path, 'tests_with_stats.v')
391 if v.pref.backend.is_js() {
392 user_files << os.join_path(preludes_path, 'stats_import.js.v')
393 }
394 }
395 if v.pref.is_prof {
396 user_files << os.join_path(preludes_path, 'profiled_program.v')
397 }
398 is_test := v.pref.is_test
399 mut is_internal_module_test := false
400 if is_test {
401 tcontent := util.read_file(dir) or { verror('${dir} does not exist') }
402 slines := tcontent.split_into_lines()
403 for sline in slines {
404 line := sline.trim_space()
405 if line.len > 2 {
406 if line[0] == `/` && line[1] == `/` {
407 continue
408 }
409 if line.starts_with('module ') {
410 is_internal_module_test = true
411 break
412 }
413 }
414 }
415 }
416 if is_internal_module_test {
417 // v volt/slack_test.v: compile all .v files to get the environment
418 single_test_v_file := os.real_path(dir)
419 if v.pref.is_verbose {
420 v.log('> Compiling an internal module _test.v file ${single_test_v_file} .')
421 v.log('> That brings in all other ordinary .v files in the same module too .')
422 }
423 user_files << single_test_v_file
424 dir = os.dir(single_test_v_file)
425 }
426 v.add_file_or_dir(mut user_files, dir)
427 for f in v.pref.file_list {
428 file := f.trim_space()
429 if file.len > 0 {
430 v.add_file_or_dir(mut user_files, file)
431 }
432 }
433 if user_files.len == 0 {
434 println('No input .v files')
435 exit(1)
436 }
437 if v.pref.is_verbose {
438 v.log('user_files: ${user_files}')
439 }
440 return user_files
441}
442
443fn (v &Builder) add_file_or_dir(mut user_files []string, dir string) {
444 does_exist := os.exists(dir)
445 if !does_exist {
446 verror("${dir} doesn't exist")
447 }
448 is_real_file := does_exist && !os.is_dir(dir)
449 resolved_link := if is_real_file && os.is_link(dir) { os.real_path(dir) } else { dir }
450 if is_real_file && (dir.ends_with('.v') || resolved_link.ends_with('.vsh')
451 || v.pref.raw_vsh_tmp_prefix != '' || dir.ends_with('.vv')) {
452 single_v_file := if resolved_link.ends_with('.vsh') { resolved_link } else { dir }
453 // Just compile one file and get parent dir
454 user_files << single_v_file
455 if v.pref.is_verbose {
456 v.log('> add one file: "${single_v_file}"')
457 }
458 } else if os.is_dir(dir) {
459 if v.pref.is_verbose {
460 v.log('> add all .v files from directory "${dir}" ...')
461 }
462 // Add .v files from the directory being compiled
463 user_files << v.v_files_from_dir(dir)
464 } else {
465 println('usage: `v file.v` or `v directory`')
466 ext := os.file_ext(dir)
467 println('unknown file extension `${ext}`')
468 exit(1)
469 }
470}
471