diff --git a/cmd/v/help/default.txt b/cmd/v/help/default.txt index 77d3b57571..7a1c50b063 100644 --- a/cmd/v/help/default.txt +++ b/cmd/v/help/default.txt @@ -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. diff --git a/cmd/v/v.v b/cmd/v/v.v index c201516222..a52c8f9c05 100644 --- a/cmd/v/v.v +++ b/cmd/v/v.v @@ -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..]) + } + } +} diff --git a/vlib/v/builder/builder.v b/vlib/v/builder/builder.v index 07c599c3e9..bb091c459a 100644 --- a/vlib/v/builder/builder.v +++ b/vlib/v/builder/builder.v @@ -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 { diff --git a/vlib/v/builder/compile.v b/vlib/v/builder/compile.v index 9f719f7a16..a518987317 100644 --- a/vlib/v/builder/compile.v +++ b/vlib/v/builder/compile.v @@ -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 diff --git a/vlib/v/builder/rebuilding.v b/vlib/v/builder/rebuilding.v index c2f13547d1..44a5773c00 100644 --- a/vlib/v/builder/rebuilding.v +++ b/vlib/v/builder/rebuilding.v @@ -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)) +} diff --git a/vlib/v/pref/pref.v b/vlib/v/pref/pref.v index fedff0fd34..4818467287 100644 --- a/vlib/v/pref/pref.v +++ b/vlib/v/pref/pref.v @@ -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' { diff --git a/vlib/v/tests/crun_mode/crun_test.v b/vlib/v/tests/crun_mode/crun_test.v new file mode 100644 index 0000000000..2700692467 --- /dev/null +++ b/vlib/v/tests/crun_mode/crun_test.v @@ -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' +}