v: add support for `v crun examples/hello_world.v`, use crun mode for .vsh files by default. (#14554)

master
Delyan Angelov 2022-06-01 14:47:52 +03:00 committed by GitHub
parent c91b646372
commit bf70f0b436
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 300 additions and 137 deletions

View File

@ -7,6 +7,7 @@ Examples:
v hello.v Compile the file `hello.v` and output it as `hello` or `hello.exe`.
v run hello.v Same as above but also run the produced executable immediately after compilation.
v -cg run hello.v Same as above, but make debugging easier (in case your program crashes).
v crun hello.v Same as above, but do not recompile, if the executable already exists, and is newer than the sources.
v -o h.c hello.v Translate `hello.v` to `h.c`. Do not compile further.
v -o - hello.v Translate `hello.v` and output the C source code to stdout. Do not compile further.
@ -20,7 +21,10 @@ V supports the following commands:
init Setup the file structure for an already existing V project.
* Ordinary development:
run Compile and run a V program.
run Compile and run a V program. Delete the executable after the run.
crun Compile and run a V program without deleting the executable.
If you run the same program a second time, without changing the source files,
V will just run the executable, without recompilation. Suitable for scripting.
test Run all test files in the provided directory.
fmt Format the V code provided.
vet Report suspicious code constructs.

View File

@ -93,17 +93,21 @@ fn main() {
return
}
match command {
'run', 'crun', 'build', 'build-module' {
rebuild(prefs)
return
}
'help' {
invoke_help_and_exit(args)
}
'version' {
println(version.full_v_version(prefs.is_verbose))
return
}
'new', 'init' {
util.launch_tool(prefs.is_verbose, 'vcreate', os.args[1..])
return
}
'translate' {
eprintln('Translating C to V will be available in V 0.3')
exit(1)
}
'install', 'list', 'outdated', 'remove', 'search', 'show', 'update', 'upgrade' {
util.launch_tool(prefs.is_verbose, 'vpm', os.args[1..])
return
@ -118,42 +122,24 @@ fn main() {
eprintln('V Error: Use `v install` to install modules from vpm.vlang.io')
exit(1)
}
'version' {
println(version.full_v_version(prefs.is_verbose))
return
'translate' {
eprintln('Translating C to V will be available in V 0.3')
exit(1)
}
else {}
}
if command in ['run', 'build', 'build-module'] || command.ends_with('.v') || os.exists(command) {
// println('command')
// println(prefs.path)
match prefs.backend {
.c {
$if no_bootstrapv ? {
// TODO: improve the bootstrapping with a split C backend here.
// C code generated by `VEXE=v cmd/tools/builders/c_builder -os cross -o c.c cmd/tools/builders/c_builder.v`
// is enough to bootstrap the C backend, and thus the rest, but currently bootstrapping relies on
// `v -os cross -o v.c cmd/v` having a functional C codegen inside instead.
util.launch_tool(prefs.is_verbose, 'builders/c_builder', os.args[1..])
}
builder.compile('build', prefs, cbuilder.compile_c)
}
.js_node, .js_freestanding, .js_browser {
util.launch_tool(prefs.is_verbose, 'builders/js_builder', os.args[1..])
}
.native {
util.launch_tool(prefs.is_verbose, 'builders/native_builder', os.args[1..])
}
.interpret {
util.launch_tool(prefs.is_verbose, 'builders/interpret_builder', os.args[1..])
else {
if command.ends_with('.v') || os.exists(command) {
// println('command')
// println(prefs.path)
rebuild(prefs)
return
}
}
return
}
if prefs.is_help {
invoke_help_and_exit(args)
}
eprintln('v $command: unknown command\nRun ${term.highlight_command('v help')} for usage.')
eprintln('v $command: unknown command')
eprintln('Run ${term.highlight_command('v help')} for usage.')
exit(1)
}
@ -163,7 +149,31 @@ fn invoke_help_and_exit(remaining []string) {
2 { help.print_and_exit(remaining[1]) }
else {}
}
println('${term.highlight_command('v help')}: provide only one help topic.')
println('For usage information, use ${term.highlight_command('v help')}.')
eprintln('${term.highlight_command('v help')}: provide only one help topic.')
eprintln('For usage information, use ${term.highlight_command('v help')}.')
exit(1)
}
fn rebuild(prefs &pref.Preferences) {
match prefs.backend {
.c {
$if no_bootstrapv ? {
// TODO: improve the bootstrapping with a split C backend here.
// C code generated by `VEXE=v cmd/tools/builders/c_builder -os cross -o c.c cmd/tools/builders/c_builder.v`
// is enough to bootstrap the C backend, and thus the rest, but currently bootstrapping relies on
// `v -os cross -o v.c cmd/v` having a functional C codegen inside instead.
util.launch_tool(prefs.is_verbose, 'builders/c_builder', os.args[1..])
}
builder.compile('build', prefs, cbuilder.compile_c)
}
.js_node, .js_freestanding, .js_browser {
util.launch_tool(prefs.is_verbose, 'builders/js_builder', os.args[1..])
}
.native {
util.launch_tool(prefs.is_verbose, 'builders/native_builder', os.args[1..])
}
.interpret {
util.launch_tool(prefs.is_verbose, 'builders/interpret_builder', os.args[1..])
}
}
}

View File

@ -17,7 +17,7 @@ import v.dotgraph
pub struct Builder {
pub:
compiled_dir string // contains os.real_path() of the dir of the final file beeing compiled, or the dir itself when doing `v .`
compiled_dir string // contains os.real_path() of the dir of the final file being compiled, or the dir itself when doing `v .`
module_path string
pub mut:
checker &checker.Checker
@ -40,6 +40,7 @@ pub mut:
mod_invalidates_paths map[string][]string // changes in mod `os`, invalidate only .v files, that do `import os`
mod_invalidates_mods map[string][]string // changes in mod `os`, force invalidation of mods, that do `import os`
path_invalidates_mods map[string][]string // changes in a .v file from `os`, invalidates `os`
crun_cache_keys []string // target executable + top level source files; filled in by Builder.should_rebuild
}
pub fn new_builder(pref &pref.Preferences) Builder {

View File

@ -3,9 +3,7 @@
// that can be found in the LICENSE file.
module builder
import time
import os
import rand
import v.pref
import v.util
import v.checker
@ -13,6 +11,19 @@ import v.checker
pub type FnBackend = fn (mut b Builder)
pub fn compile(command string, pref &pref.Preferences, backend_cb FnBackend) {
check_if_output_folder_is_writable(pref)
// Construct the V object from command line arguments
mut b := new_builder(pref)
if b.should_rebuild() {
b.rebuild(backend_cb)
}
b.exit_on_invalid_syntax()
// running does not require the parsers anymore
unsafe { b.myfree() }
b.run_compiled_executable_and_exit()
}
fn check_if_output_folder_is_writable(pref &pref.Preferences) {
odir := os.dir(pref.out_name)
// When pref.out_name is just the name of an executable, i.e. `./v -o executable main.v`
// without a folder component, just use the current folder instead:
@ -24,56 +35,6 @@ pub fn compile(command string, pref &pref.Preferences, backend_cb FnBackend) {
// An early error here, is better than an unclear C error later:
verror(err.msg())
}
// Construct the V object from command line arguments
mut b := new_builder(pref)
if pref.is_verbose {
println('builder.compile() pref:')
// println(pref)
}
mut sw := time.new_stopwatch()
backend_cb(mut b)
mut timers := util.get_timers()
timers.show_remaining()
if 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 := 0, 0
for pf in b.parsed_files {
all_v_source_lines += pf.nr_lines
all_v_source_bytes += pf.nr_bytes
}
mut sall_v_source_lines := all_v_source_lines.str()
mut sall_v_source_bytes := all_v_source_bytes.str()
sall_v_source_lines = util.bold('${sall_v_source_lines:10s}')
sall_v_source_bytes = util.bold('${sall_v_source_bytes:10s}')
println(' V source code size: $sall_v_source_lines lines, $sall_v_source_bytes bytes')
//
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())
println('compilation took: $scompilation_time_ms ms, compilation speed: $svlines_per_second vlines/s')
}
b.exit_on_invalid_syntax()
// running does not require the parsers anymore
unsafe { b.myfree() }
if pref.is_test || pref.is_run {
b.run_compiled_executable_and_exit()
}
}
pub fn (mut b Builder) get_vtmp_filename(base_file_name string, postfix string) string {
vtmp := util.get_vtmp_folder()
mut uniq := ''
if !b.pref.reuse_tmpc {
uniq = '.$rand.u64()'
}
fname := os.file_name(os.real_path(base_file_name)) + '$uniq$postfix'
return os.real_path(os.join_path(vtmp, fname))
}
// Temporary, will be done by -autofree
@ -118,47 +79,45 @@ fn (mut b Builder) run_compiled_executable_and_exit() {
if b.pref.os == .ios {
panic('Running iOS apps is not supported yet.')
}
if !(b.pref.is_test || b.pref.is_run || b.pref.is_crun) {
exit(0)
}
compiled_file := os.real_path(b.pref.out_name)
run_file := if b.pref.backend.is_js() {
node_basename := $if windows { 'node.exe' } $else { 'node' }
os.find_abs_path_of_executable(node_basename) or {
panic('Could not find `node` in system path. Do you have Node.js installed?')
}
} else {
compiled_file
}
mut run_args := []string{cap: b.pref.run_args.len + 1}
if b.pref.backend.is_js() {
run_args << compiled_file
}
run_args << b.pref.run_args
mut run_process := os.new_process(run_file)
run_process.set_args(run_args)
if b.pref.is_verbose {
println('running $run_process.filename with arguments $run_process.args')
}
if b.pref.is_test || b.pref.is_run {
compiled_file := os.real_path(b.pref.out_name)
run_file := if b.pref.backend.is_js() {
node_basename := $if windows { 'node.exe' } $else { 'node' }
os.find_abs_path_of_executable(node_basename) or {
panic('Could not find `node` in system path. Do you have Node.js installed?')
}
} else {
compiled_file
}
mut run_args := []string{cap: b.pref.run_args.len + 1}
if b.pref.backend.is_js() {
run_args << compiled_file
}
run_args << b.pref.run_args
mut run_process := os.new_process(run_file)
run_process.set_args(run_args)
if b.pref.is_verbose {
println('running $run_process.filename with arguments $run_process.args')
}
// Ignore sigint and sigquit while running the compiled file,
// so ^C doesn't prevent v from deleting the compiled file.
// See also https://git.musl-libc.org/cgit/musl/tree/src/process/system.c
prev_int_handler := os.signal_opt(.int, eshcb) or { serror('set .int', err) }
mut prev_quit_handler := os.SignalHandler(eshcb)
$if !windows { // There's no sigquit on windows
prev_quit_handler = os.signal_opt(.quit, eshcb) or { serror('set .quit', err) }
}
run_process.wait()
os.signal_opt(.int, prev_int_handler) or { serror('restore .int', err) }
$if !windows {
os.signal_opt(.quit, prev_quit_handler) or { serror('restore .quit', err) }
}
ret := run_process.code
run_process.close()
b.cleanup_run_executable_after_exit(compiled_file)
exit(ret)
// Ignore sigint and sigquit while running the compiled file,
// so ^C doesn't prevent v from deleting the compiled file.
// See also https://git.musl-libc.org/cgit/musl/tree/src/process/system.c
prev_int_handler := os.signal_opt(.int, eshcb) or { serror('set .int', err) }
mut prev_quit_handler := os.SignalHandler(eshcb)
$if !windows { // There's no sigquit on windows
prev_quit_handler = os.signal_opt(.quit, eshcb) or { serror('set .quit', err) }
}
exit(0)
run_process.wait()
os.signal_opt(.int, prev_int_handler) or { serror('restore .int', err) }
$if !windows {
os.signal_opt(.quit, prev_quit_handler) or { serror('restore .quit', err) }
}
ret := run_process.code
run_process.close()
b.cleanup_run_executable_after_exit(compiled_file)
exit(ret)
}
fn eshcb(_ os.Signal) {
@ -171,6 +130,9 @@ fn serror(reason string, e IError) {
}
fn (mut v Builder) cleanup_run_executable_after_exit(exefile string) {
if v.pref.is_crun {
return
}
if v.pref.reuse_tmpc {
v.pref.vrun_elog('keeping executable: $exefile , because -keepc was passed')
return

View File

@ -2,6 +2,8 @@ module builder
import os
import hash
import time
import rand
import strings
import v.util
import v.pref
@ -11,11 +13,27 @@ pub fn (mut b Builder) rebuild_modules() {
if !b.pref.use_cache || b.pref.build_mode == .build_module {
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)
all_files := b.parsed_files.map(it.path)
//
mut cm := vcache.new_cache_manager(all_files)
sold_hashes := cm.load('.hashes', 'all_files') or { ' ' }
@ -31,8 +49,7 @@ pub fn (mut b Builder) rebuild_modules() {
old_hashes[cpath] = chash
}
// eprintln('old_hashes: $old_hashes')
for p in b.parsed_files {
cpath := p.path
for cpath in all_files {
ccontent := util.read_file(cpath) or { '' }
chash := hash.sum64_string(ccontent, 7).hex_full()
new_hashes[cpath] = chash
@ -48,6 +65,7 @@ pub fn (mut b Builder) rebuild_modules() {
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')
@ -148,13 +166,13 @@ pub fn (mut b Builder) rebuild_modules() {
}
if invalidated_mod_paths.len > 0 {
impaths := invalidated_mod_paths.keys()
vexe := pref.vexe_path()
for imp in impaths {
b.v_build_module(vexe, imp)
invalidations << imp
}
}
util.timing_measure('${@METHOD} rebuilding')
}
return invalidations
}
fn (mut b Builder) v_build_module(vexe string, imp_path string) {
@ -237,3 +255,114 @@ fn (mut b Builder) handle_usecache(vexe string) {
}
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_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.pref.build_options.join('\n') {
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')
// we have already compiled these source files, and have their dependencies
dependencies_stamp := most_recent_timestamp(dependencies)
if dependencies_stamp < exe_stamp {
return false
}
return true
}
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.parsed_files.map(it.path)
cm.save('.dependencies', '.crun', dependency_files.join('\n')) or {}
cm.save('.build_options', '.crun', b.pref.build_options.join('\n')) 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 := 0, 0
for pf in b.parsed_files {
all_v_source_lines += pf.nr_lines
all_v_source_bytes += pf.nr_bytes
}
mut sall_v_source_lines := all_v_source_lines.str()
mut sall_v_source_bytes := all_v_source_bytes.str()
sall_v_source_lines = util.bold('${sall_v_source_lines:10s}')
sall_v_source_bytes = util.bold('${sall_v_source_bytes:10s}')
println(' V source code size: $sall_v_source_lines lines, $sall_v_source_bytes bytes')
//
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())
println('compilation took: $scompilation_time_ms ms, compilation speed: $svlines_per_second vlines/s')
}
}
pub fn (mut b Builder) get_vtmp_filename(base_file_name string, postfix string) string {
vtmp := util.get_vtmp_folder()
mut uniq := ''
if !b.pref.reuse_tmpc {
uniq = '.$rand.u64()'
}
fname := os.file_name(os.real_path(base_file_name)) + '$uniq$postfix'
return os.real_path(os.join_path(vtmp, fname))
}

View File

@ -111,7 +111,8 @@ pub mut:
is_prof bool // benchmark every function
is_prod bool // use "-O2"
is_repl bool
is_run bool
is_run bool // compile and run a v program, passing arguments to it, and deleting the executable afterwards
is_crun bool // similar to run, but does not recompile the executable, if there were no changes to the sources
is_debug bool // turned on by -g or -cg, it tells v to pass -g to the C backend compiler.
is_vlines bool // turned on by -g (it slows down .tmp.c generation slightly).
is_stats bool // `v -stats file_test.v` will produce more detailed statistics for the tests that were run
@ -706,7 +707,7 @@ pub fn parse_args_and_show_errors(known_external_commands []string, args []strin
if command == '' {
command = arg
command_pos = i
if command == 'run' {
if command in ['run', 'crun'] {
break
}
} else if is_source_file(command) && is_source_file(arg)
@ -734,6 +735,12 @@ pub fn parse_args_and_show_errors(known_external_commands []string, args []strin
if res.is_debug {
res.parse_define('debug')
}
if command == 'crun' {
res.is_crun = true
}
if command == 'run' {
res.is_run = true
}
if command == 'run' && res.is_prod && os.is_atty(1) > 0 {
eprintln_cond(show_output, "Note: building an optimized binary takes much longer. It shouldn't be used with `v run`.")
eprintln_cond(show_output, 'Use `v run` without optimization, or build an optimized binary with -prod first, then run it separately.')
@ -744,8 +751,7 @@ pub fn parse_args_and_show_errors(known_external_commands []string, args []strin
eprintln('Cannot save output binary in a .v file.')
exit(1)
}
if command == 'run' {
res.is_run = true
if res.is_run || res.is_crun {
if command_pos + 2 > args.len {
eprintln('v run: no v files listed')
exit(1)
@ -798,7 +804,7 @@ pub fn parse_args_and_show_errors(known_external_commands []string, args []strin
// `v build.vsh gcc` is the same as `v run build.vsh gcc`,
// i.e. compiling, then running the script, passing the args
// after it to the script:
res.is_run = true
res.is_crun = true
res.path = command
res.run_args = args[command_pos + 1..]
} else if command == 'interpret' {

View File

@ -0,0 +1,51 @@
import os
import time
const crun_folder = os.join_path(os.temp_dir(), 'crun_folder')
const vprogram_file = os.join_path(crun_folder, 'vprogram.vv')
const vexe = os.getenv('VEXE')
fn testsuite_begin() {
os.setenv('VCACHE', crun_folder, true)
os.rmdir_all(crun_folder) or {}
os.mkdir_all(crun_folder) or {}
assert os.is_dir(crun_folder)
}
fn testsuite_end() {
os.chdir(os.wd_at_startup) or {}
os.rmdir_all(crun_folder) or {}
assert !os.is_dir(crun_folder)
}
fn test_saving_simple_v_program() ? {
os.write_file(vprogram_file, 'print("hello")')?
assert true
}
fn test_crun_simple_v_program_several_times() ? {
mut sw := time.new_stopwatch()
mut times := []i64{}
for i in 0 .. 10 {
vcrun()
times << sw.elapsed().microseconds()
time.sleep(50 * time.millisecond)
sw.restart()
}
dump(times)
assert times.first() > times.last() * 5 // cruns compile just once, if the source file is not changed
$if !windows {
os.system('ls -la $crun_folder')
os.system('find $crun_folder')
}
}
fn vcrun() {
cmd := '${os.quoted_path(vexe)} crun ${os.quoted_path(vprogram_file)}'
eprintln('now: $time.now().format_ss_milli() | cmd: $cmd')
res := os.execute(cmd)
assert res.exit_code == 0
assert res.output == 'hello'
}