| 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. |
| 4 | module util |
| 5 | |
| 6 | import os |
| 7 | import os.filelock |
| 8 | import term |
| 9 | import rand |
| 10 | import time |
| 11 | import v.pref |
| 12 | import v.util.recompilation |
| 13 | import v.util.vflags |
| 14 | import runtime |
| 15 | |
| 16 | // math.bits is needed by strconv.ftoa |
| 17 | pub const builtin_module_parts = ['math.bits', 'strconv', 'dlmalloc', 'strconv.ftoa', 'strings', |
| 18 | 'builtin', 'builtin.closure', 'builtin.overflow'] |
| 19 | pub const bundle_modules = ['clipboard', 'fontstash', 'gg', 'gx', 'sokol', 'szip', 'ui', |
| 20 | 'builtin.closure', 'builtin.overflow']! |
| 21 | |
| 22 | pub const external_module_dependencies_for_tool = { |
| 23 | 'vdoc': ['markdown'] |
| 24 | } |
| 25 | |
| 26 | const const_tabs = [ |
| 27 | '', |
| 28 | '\t', |
| 29 | '\t\t', |
| 30 | '\t\t\t', |
| 31 | '\t\t\t\t', |
| 32 | '\t\t\t\t\t', |
| 33 | '\t\t\t\t\t\t', |
| 34 | '\t\t\t\t\t\t\t', |
| 35 | '\t\t\t\t\t\t\t\t', |
| 36 | '\t\t\t\t\t\t\t\t\t', |
| 37 | '\t\t\t\t\t\t\t\t\t\t', |
| 38 | ]! |
| 39 | |
| 40 | pub const nr_jobs = runtime.nr_jobs() |
| 41 | |
| 42 | pub fn module_is_builtin(mod string) bool { |
| 43 | return mod in builtin_module_parts |
| 44 | } |
| 45 | |
| 46 | @[direct_array_access] |
| 47 | pub fn tabs(n int) string { |
| 48 | return if n >= 0 && n < const_tabs.len { const_tabs[n] } else { '\t'.repeat(n) } |
| 49 | } |
| 50 | |
| 51 | pub const stable_build_time = get_build_time() |
| 52 | // get_build_time returns the current build time, while taking into account SOURCE_DATE_EPOCH |
| 53 | // to support transparent reproducible builds. See also https://reproducible-builds.org/docs/source-date-epoch/ |
| 54 | // When SOURCE_DATE_EPOCH is not set, it will return the current UTC time. |
| 55 | pub fn get_build_time() time.Time { |
| 56 | sde := os.getenv('SOURCE_DATE_EPOCH') |
| 57 | if sde == '' { |
| 58 | return time.utc() |
| 59 | } |
| 60 | return time.unix_nanosecond(sde.i64(), 0) |
| 61 | } |
| 62 | |
| 63 | // set_vroot_folder sets the VCHILD env variable to 'true', and VEXE to the location of the V executable |
| 64 | // It is called very early by launch_tool/3, so that those env variables can be available by all tools, |
| 65 | // like `v doc`, `v fmt` etc, so they can use them to find how they were started. |
| 66 | pub fn set_vroot_folder(vroot_path string) { |
| 67 | // Preparation for the compiler module: |
| 68 | // VEXE env variable is needed so that compiler.vexe_path() can return it |
| 69 | // later to whoever needs it. Note: guessing is a heuristic, so only try to |
| 70 | // guess the V executable name, if VEXE has not been set already. |
| 71 | vexe := os.getenv('VEXE') |
| 72 | if vexe == '' { |
| 73 | vname := if os.user_os() == 'windows' { 'v.exe' } else { 'v' } |
| 74 | os.setenv('VEXE', os.real_path(os.join_path_single(vroot_path, vname)), true) |
| 75 | } |
| 76 | os.setenv('VCHILD', 'true', true) |
| 77 | } |
| 78 | |
| 79 | fn tool_recompilation_args(tool_name string, user_os string) []string { |
| 80 | if user_os == 'freebsd' && tool_name == 'vdoc' { |
| 81 | // FreeBSD's default tcc setup can not compile vdoc reliably. |
| 82 | return ['-cc', 'cc'] |
| 83 | } |
| 84 | if tool_name == 'vpm' && user_os == 'macos' { |
| 85 | // vpm performs HTTPS requests against the package registry, so it |
| 86 | // pulls in the TLS layer. Build it against OpenSSL instead of the |
| 87 | // bundled mbedtls: tcc miscompiles mbedtls big-int routines on |
| 88 | // Apple Silicon, which causes TLS handshakes to spin forever in |
| 89 | // mbedtls_mpi_sub_abs / ecp_modp (e.g. `v install sdl` would hang). |
| 90 | // Restricted to macOS: musl-based Linux builds lack the glibc |
| 91 | // headers OpenSSL pulls in (e.g. `sys/cdefs.h`), so forcing it |
| 92 | // there breaks `v install` in the docker-ubuntu-musl CI. |
| 93 | return ['-d', 'use_openssl'] |
| 94 | } |
| 95 | return []string{} |
| 96 | } |
| 97 | |
| 98 | fn temporary_tool_executable_path(vroot string, tool_name string) string { |
| 99 | sanitized_vroot := vroot.replace_each(['\\', '_', '/', '_', ':', '_']) |
| 100 | return path_of_executable(os.join_path(os.vtmp_dir(), 'tools', sanitized_vroot, tool_name)) |
| 101 | } |
| 102 | |
| 103 | fn fallback_tool_executable_path(vexe string, vroot string, tool_name string, tool_source string, tool_exe string, is_recompilation_disabled bool) string { |
| 104 | if !os.is_file(tool_source) { |
| 105 | return tool_exe |
| 106 | } |
| 107 | temporary_tool_exe := temporary_tool_executable_path(vroot, tool_name) |
| 108 | if is_recompilation_disabled && !os.exists(tool_exe) { |
| 109 | return temporary_tool_exe |
| 110 | } |
| 111 | tool_exe_dir := os.dir(tool_exe) |
| 112 | if os.is_writable(tool_exe_dir) { |
| 113 | return tool_exe |
| 114 | } |
| 115 | // Reuse a writable temp location when the packaged tool can not be updated in place. |
| 116 | if !os.exists(tool_exe) || os.exists(temporary_tool_exe) |
| 117 | || should_recompile_tool(vexe, tool_source, tool_name, tool_exe) { |
| 118 | return temporary_tool_exe |
| 119 | } |
| 120 | return tool_exe |
| 121 | } |
| 122 | |
| 123 | // is_escape_sequence returns `true` if `c` is considered a valid escape sequence denoter. |
| 124 | @[inline] |
| 125 | pub fn is_escape_sequence(c u8) bool { |
| 126 | return c in [`x`, `u`, `e`, `n`, `r`, `t`, `v`, `a`, `f`, `b`, `\\`, `\``, `$`, `@`, `?`, `{`, |
| 127 | `}`, `'`, `"`, `U`] |
| 128 | } |
| 129 | |
| 130 | // launch_tool - starts a V tool in a separate process, passing it the `args`. |
| 131 | // All V tools are located in the cmd/tools folder, in files or folders prefixed by |
| 132 | // the letter `v`, followed by the tool name, i.e. `cmd/tools/vdoc/` or `cmd/tools/vpm.v`. |
| 133 | // The folder variant is suitable for larger and more complex tools, like `v doc`, because |
| 134 | // it provides you the ability to split their source in separate .v files, organized by topic, |
| 135 | // as well as have resources like static css/text/js files, that the tools can use. |
| 136 | // launch_tool uses a timestamp based detection mechanism, so that after `v self`, each tool |
| 137 | // will be recompiled too, before it is used, which guarantees that it would be up to date with |
| 138 | // V itself. That mechanism can be disabled by package managers by creating/touching a small |
| 139 | // `cmd/tools/.disable_autorecompilation` file, OR by changing the timestamps of all executables |
| 140 | // in cmd/tools to be < 1024 seconds (in unix time). |
| 141 | @[noreturn] |
| 142 | pub fn launch_tool(is_verbose bool, tool_name string, args []string) { |
| 143 | vexe := pref.vexe_path() |
| 144 | vroot := os.dir(vexe) |
| 145 | set_vroot_folder(vroot) |
| 146 | tool_args := args_quote_paths(args) |
| 147 | tools_folder := os.join_path(vroot, 'cmd', 'tools') |
| 148 | tool_basename := os.real_path(os.join_path_single(tools_folder, tool_name)) |
| 149 | mut tool_exe := '' |
| 150 | mut tool_source := '' |
| 151 | if os.is_dir(tool_basename) { |
| 152 | tool_exe = path_of_executable(os.join_path_single(tool_basename, os.file_name(tool_name))) |
| 153 | tool_source = tool_basename |
| 154 | } else { |
| 155 | tool_exe = path_of_executable(tool_basename) |
| 156 | tool_source = tool_basename + '.v' |
| 157 | } |
| 158 | original_tool_exe := tool_exe |
| 159 | if is_verbose { |
| 160 | println('launch_tool vexe : ${vexe}') |
| 161 | println('launch_tool vroot : ${vroot}') |
| 162 | println('launch_tool tool_source : ${tool_source}') |
| 163 | println('launch_tool tool_exe : ${tool_exe}') |
| 164 | println('launch_tool tool_args : ${tool_args}') |
| 165 | } |
| 166 | disabling_file := recompilation.disabling_file(vroot) |
| 167 | is_recompilation_disabled := os.exists(disabling_file) |
| 168 | tool_exe = fallback_tool_executable_path(vexe, vroot, tool_name, tool_source, tool_exe, |
| 169 | is_recompilation_disabled) |
| 170 | is_using_temporary_tool_exe := tool_exe != original_tool_exe |
| 171 | if !os.exists(tool_exe) && !os.exists(tool_source) { |
| 172 | eprintln('cannot find `${tool_name}`: missing both `${tool_exe}` and `${tool_source}`') |
| 173 | exit(1) |
| 174 | } |
| 175 | if !os.exists(tool_exe) && is_recompilation_disabled && !is_using_temporary_tool_exe { |
| 176 | eprintln('cannot find the prebuilt `${tool_name}` tool at `${tool_exe}`') |
| 177 | eprintln('Automatic tool recompilation is disabled by "${disabling_file}".') |
| 178 | eprintln('Please reinstall V from a complete package, or install V from source.') |
| 179 | exit(1) |
| 180 | } |
| 181 | should_compile := (!is_recompilation_disabled || is_using_temporary_tool_exe) |
| 182 | && should_recompile_tool(vexe, tool_source, tool_name, tool_exe) |
| 183 | if is_verbose { |
| 184 | println('launch_tool should_compile: ${should_compile}') |
| 185 | } |
| 186 | if should_compile { |
| 187 | emodules := external_module_dependencies_for_tool[tool_name] |
| 188 | for emodule in emodules { |
| 189 | check_module_is_installed(emodule, is_verbose, false) or { panic(err) } |
| 190 | } |
| 191 | mut compilation_command := '${os.quoted_path(vexe)} ' |
| 192 | if tool_name in ['vself', 'vup', 'vdoctor', 'vsymlink'] { |
| 193 | // These tools will be called by users in cases where there |
| 194 | // is high chance of there being a problem somewhere. Thus |
| 195 | // it is better to always compile them with -g, so that in |
| 196 | // case these tools do crash/panic, their backtraces will have |
| 197 | // .v line numbers, to ease diagnostic in #bugs and issues. |
| 198 | compilation_command += ' -g ' |
| 199 | } |
| 200 | if tool_name == 'vfmt' { |
| 201 | compilation_command += ' -d vfmt ' |
| 202 | } |
| 203 | compilation_args := tool_recompilation_args(tool_name, os.user_os()) |
| 204 | if compilation_args.len > 0 { |
| 205 | compilation_command += ' ${args_quote_paths(compilation_args)} ' |
| 206 | } |
| 207 | if is_using_temporary_tool_exe { |
| 208 | tmp_tool_dir := os.dir(tool_exe) |
| 209 | if !os.is_dir(tmp_tool_dir) { |
| 210 | os.mkdir_all(tmp_tool_dir) or { |
| 211 | eprintln('cannot prepare temporary tool folder `${tmp_tool_dir}`: ${err}') |
| 212 | exit(1) |
| 213 | } |
| 214 | } |
| 215 | } |
| 216 | compilation_command += ' -o ${os.quoted_path(tool_exe)} ' |
| 217 | compilation_command += os.quoted_path(tool_source) |
| 218 | if is_verbose { |
| 219 | println('Compiling ${tool_name} with: "${compilation_command}"') |
| 220 | } |
| 221 | |
| 222 | current_work_dir := os.getwd() |
| 223 | tlog('recompiling ${tool_source}') |
| 224 | lockfile := tool_exe + '.lock' |
| 225 | tlog('lockfile: ${lockfile}') |
| 226 | mut l := filelock.new(lockfile) |
| 227 | if l.try_acquire() { |
| 228 | tlog('lockfile acquired') |
| 229 | tool_recompile_retry_max_count := int_max(1, os.getenv_opt('VUTIL_RETRY_MAX_COUNT') or { |
| 230 | '7' |
| 231 | }.int()) |
| 232 | for i in 0 .. tool_recompile_retry_max_count { |
| 233 | tlog('looping i: ${i} / ${tool_recompile_retry_max_count}') |
| 234 | // ensure a stable and known working folder, when compiling V's tools, to avoid module lookup problems: |
| 235 | os.chdir(vroot) or {} |
| 236 | compile_sw := time.new_stopwatch() |
| 237 | tool_compilation := os.execute(compilation_command) |
| 238 | os.chdir(current_work_dir) or {} |
| 239 | tlog('tool_compilation.exit_code: ${tool_compilation.exit_code}, took: ${compile_sw.elapsed().milliseconds()}ms') |
| 240 | if tool_compilation.exit_code == 0 { |
| 241 | break |
| 242 | } else { |
| 243 | if tool_name == 'vup' { |
| 244 | eprintln('Cannot recompile the new version of `vup`: ${tool_compilation.exit_code}\n${tool_compilation.output}') |
| 245 | if os.exists(tool_exe) { |
| 246 | // Compilation failed, but we still have an already existing old `vup.exe`, that *probably* works. |
| 247 | // It is better to pretend the compilation succeeded, and try the old executable, then let it fail |
| 248 | // on its own, if it can not work too (it will produce a nicer diagnostic message), than to fail here |
| 249 | // right away, just because the new source is too breaking, for the older V frontend process, |
| 250 | // that is currently running :-| |
| 251 | eprintln('Trying an already existing old version of the `vup` tool instead...') |
| 252 | break |
| 253 | } else { |
| 254 | // No pre-existing `vup.exe` ... No choice but to show a message to the user :-( . |
| 255 | // Note: running `make` here from within the old V frontend process, is not reliable, since it can fail |
| 256 | // on windows and probably other systems, because the currently running executable is locked. |
| 257 | // `vup.exe` has logic to workaround that, and duplicating it here, is hard to debug/diagnose. |
| 258 | eprintln('Failed compilation of the `vup` tool, using the new V source code.') |
| 259 | eprintln('The new source code, is likely to be unsupported, by your existing older V executable.') |
| 260 | eprintln('Try running `make` or `makev.bat` manually.') |
| 261 | eprintln('If that fails, clone V from source in a new folder, and run `make` or `makev.bat` manually again there.') |
| 262 | l.release() |
| 263 | exit(1) |
| 264 | } |
| 265 | } |
| 266 | if i == tool_recompile_retry_max_count - 1 { |
| 267 | eprintln('cannot compile `${tool_source}`: ${tool_compilation.exit_code}\n${tool_compilation.output}') |
| 268 | l.release() |
| 269 | exit(1) |
| 270 | } |
| 271 | } |
| 272 | time.sleep((20 + rand.intn(40) or { 0 }) * time.millisecond) |
| 273 | } |
| 274 | tlog('lockfile releasing') |
| 275 | l.release() |
| 276 | tlog('lockfile released') |
| 277 | } else { |
| 278 | tlog('another process got the lock') |
| 279 | // wait till the other V tool recompilation process finished; |
| 280 | // the timeout is intentionally generous, since on slow CI VMs |
| 281 | // (e.g. FreeBSD QEMU), recompiling a tool can take >10s, and |
| 282 | // falling through with a missing tool_exe leads to ENOENT on exec: |
| 283 | if l.wait_acquire(60 * time.second) { |
| 284 | tlog('the other process finished') |
| 285 | l.release() |
| 286 | } else { |
| 287 | tlog('timeout...') |
| 288 | } |
| 289 | time.sleep((50 + rand.intn(40) or { 0 }) * time.millisecond) |
| 290 | tlog('result of the other process compiling ${tool_exe}: ${os.exists(tool_exe)}') |
| 291 | } |
| 292 | } |
| 293 | tlog('executing: ${tool_exe} with ${tool_args}') |
| 294 | $if windows { |
| 295 | cmd_system('${os.quoted_path(tool_exe)} ${tool_args}') |
| 296 | } $else $if js { |
| 297 | // no way to implement os.execvp in JS backend |
| 298 | cmd_system('${tool_exe} ${tool_args}') |
| 299 | } $else { |
| 300 | os.execvp(tool_exe, args) or { |
| 301 | eprintln('> error while executing: ${tool_exe} ${args}') |
| 302 | eprintln('> ${err}') |
| 303 | exit(1) |
| 304 | } |
| 305 | } |
| 306 | exit(2) |
| 307 | } |
| 308 | |
| 309 | @[if trace_launch_tool ?] |
| 310 | fn tlog(s string) { |
| 311 | ts := time.now().format_ss_micro() |
| 312 | eprintln('${term.yellow(ts)} ${term.gray(s)}') |
| 313 | } |
| 314 | |
| 315 | @[noreturn] |
| 316 | fn cmd_system(cmd string) { |
| 317 | res := os.system(cmd) |
| 318 | if res != 0 { |
| 319 | tlog('> error ${res}, while executing: ${cmd}') |
| 320 | } |
| 321 | exit(res) |
| 322 | } |
| 323 | |
| 324 | // Note: should_recompile_tool/4 compares unix timestamps that have 1 second resolution |
| 325 | // That means that a tool can get recompiled twice, if called in short succession. |
| 326 | // TODO: use a nanosecond mtime timestamp, if available. |
| 327 | pub fn should_recompile_tool(vexe string, tool_source string, tool_name string, tool_exe string) bool { |
| 328 | if os.is_dir(tool_source) { |
| 329 | source_files := os.walk_ext(tool_source, '.v') |
| 330 | mut newest_sfile := '' |
| 331 | mut newest_sfile_mtime := i64(0) |
| 332 | for sfile in source_files { |
| 333 | mtime := os.file_last_mod_unix(sfile) |
| 334 | if mtime > newest_sfile_mtime { |
| 335 | newest_sfile_mtime = mtime |
| 336 | newest_sfile = sfile |
| 337 | } |
| 338 | } |
| 339 | single_file_recompile := should_recompile_tool(vexe, newest_sfile, tool_name, tool_exe) |
| 340 | // eprintln('>>> should_recompile_tool: tool_source: ${tool_source} | ${single_file_recompile} | ${newest_sfile}') |
| 341 | return single_file_recompile |
| 342 | } |
| 343 | // TODO: Caching should be done on the `vlib/v` level. |
| 344 | mut should_compile := false |
| 345 | if !os.exists(tool_exe) { |
| 346 | should_compile = true |
| 347 | } else { |
| 348 | mtime_vexe := os.file_last_mod_unix(vexe) |
| 349 | mtime_tool_exe := os.file_last_mod_unix(tool_exe) |
| 350 | mtime_tool_source := os.file_last_mod_unix(tool_source) |
| 351 | if mtime_tool_exe <= mtime_vexe { |
| 352 | // v was recompiled, maybe after v up ... |
| 353 | // rebuild the tool too just in case |
| 354 | should_compile = true |
| 355 | if tool_name == 'vself' || tool_name == 'vup' { |
| 356 | // The purpose of vself/up is to update and recompile v itself. |
| 357 | // After the first 'v self' execution, v will be modified, so |
| 358 | // then a second 'v self' will detect, that v is newer than the |
| 359 | // vself executable, and try to recompile vself/up again, which |
| 360 | // will slow down the next v recompilation needlessly. |
| 361 | should_compile = false |
| 362 | } |
| 363 | } |
| 364 | if mtime_tool_exe <= mtime_tool_source { |
| 365 | // the user changed the source code of the tool, or git updated it: |
| 366 | should_compile = true |
| 367 | } |
| 368 | // GNU Guix and possibly other environments, have bit for bit reproducibility in mind, |
| 369 | // including filesystem attributes like modification times, so they set the modification |
| 370 | // times of executables to a small number like 0, 1 etc. In this case, we should not |
| 371 | // recompile even if other heuristics say that we should. Users in such environments, |
| 372 | // have to explicitly do: `v cmd/tools/vfmt.v`, and/or install v from source, and not |
| 373 | // use the system packaged one, if they desire to develop v itself. |
| 374 | if mtime_vexe < 1024 && mtime_tool_exe < 1024 { |
| 375 | should_compile = false |
| 376 | } |
| 377 | } |
| 378 | return should_compile |
| 379 | } |
| 380 | |
| 381 | fn tool_source2name_and_exe(tool_source string) (string, string) { |
| 382 | sfolder := os.dir(tool_source) |
| 383 | tool_name := os.base(tool_source).replace('.v', '') |
| 384 | tool_exe := os.join_path_single(sfolder, path_of_executable(tool_name)) |
| 385 | return tool_name, tool_exe |
| 386 | } |
| 387 | |
| 388 | pub fn quote_path(s string) string { |
| 389 | return os.quoted_path(s) |
| 390 | } |
| 391 | |
| 392 | pub fn args_quote_paths(args []string) string { |
| 393 | return args.map(quote_path(it)).join(' ') |
| 394 | } |
| 395 | |
| 396 | pub fn path_of_executable(path string) string { |
| 397 | $if windows { |
| 398 | return path + '.exe' |
| 399 | } |
| 400 | return path |
| 401 | } |
| 402 | |
| 403 | @[heap] |
| 404 | struct SourceCache { |
| 405 | mut: |
| 406 | sources map[string]string |
| 407 | } |
| 408 | |
| 409 | @[unsafe] |
| 410 | pub fn cached_read_source_file(path string) !string { |
| 411 | mut static cache := &SourceCache(unsafe { nil }) |
| 412 | if cache == unsafe { nil } { |
| 413 | cache = &SourceCache{} |
| 414 | } |
| 415 | |
| 416 | $if trace_cached_read_source_file ? { |
| 417 | println('cached_read_source_file ${path}') |
| 418 | } |
| 419 | if path == '' { |
| 420 | unsafe { cache.sources.free() } |
| 421 | unsafe { free(cache) } |
| 422 | cache = &SourceCache(unsafe { nil }) |
| 423 | return error('memory source file cache cleared') |
| 424 | } |
| 425 | |
| 426 | // eprintln('>> cached_read_source_file path: ${path}') |
| 427 | if res := cache.sources[path] { |
| 428 | // eprintln('>> cached') |
| 429 | $if trace_cached_read_source_file_cached ? { |
| 430 | println('cached_read_source_file cached ${path}') |
| 431 | } |
| 432 | return res |
| 433 | } |
| 434 | // eprintln('>> not cached | cache.sources.len: ${cache.sources.len}') |
| 435 | $if trace_cached_read_source_file_not_cached ? { |
| 436 | println('cached_read_source_file not cached ${path}') |
| 437 | } |
| 438 | raw_text := os.read_file(path) or { return error('failed to open ${path}') } |
| 439 | res := skip_bom(raw_text) |
| 440 | cache.sources[path] = res |
| 441 | return res |
| 442 | } |
| 443 | |
| 444 | pub fn replace_op(s string) string { |
| 445 | return match s { |
| 446 | '+' { '_plus' } |
| 447 | '-' { '_minus' } |
| 448 | '*' { '_mult' } |
| 449 | '**' { '_pow' } |
| 450 | '/' { '_div' } |
| 451 | '%' { '_mod' } |
| 452 | '[]' { '_index' } |
| 453 | '[]=' { '_index_set' } |
| 454 | '<' { '_lt' } |
| 455 | '>' { '_gt' } |
| 456 | '==' { '_eq' } |
| 457 | else { '' } |
| 458 | } |
| 459 | } |
| 460 | |
| 461 | // join_env_vflags_and_os_args returns all the arguments (the ones from the env variable VFLAGS too), passed on the command line. |
| 462 | pub fn join_env_vflags_and_os_args() []string { |
| 463 | return vflags.join_env_vflags_and_os_args() |
| 464 | } |
| 465 | |
| 466 | pub fn check_module_is_installed(modulename string, is_verbose bool, need_update bool) !bool { |
| 467 | mpath := os.join_path_single(os.vmodules_dir(), modulename) |
| 468 | mod_v_file := os.join_path_single(mpath, 'v.mod') |
| 469 | murl := 'https://github.com/vlang/${modulename}' |
| 470 | if is_verbose { |
| 471 | eprintln('check_module_is_installed: mpath: ${mpath}') |
| 472 | eprintln('check_module_is_installed: mod_v_file: ${mod_v_file}') |
| 473 | eprintln('check_module_is_installed: murl: ${murl}') |
| 474 | } |
| 475 | vexe := pref.vexe_path() |
| 476 | if os.exists(mod_v_file) { |
| 477 | if need_update { |
| 478 | update_cmd := "${os.quoted_path(vexe)} update '${modulename}'" |
| 479 | if is_verbose { |
| 480 | eprintln('check_module_is_installed: updating with ${update_cmd} ...') |
| 481 | } |
| 482 | update_res := os.execute(update_cmd) |
| 483 | if update_res.exit_code < 0 { |
| 484 | return error('can not start ${update_cmd}, error: ${update_res.output}') |
| 485 | } |
| 486 | if update_res.exit_code != 0 { |
| 487 | eprintln('Warning: `${modulename}` exists, but is not updated. |
| 488 | V will continue, since updates can fail due to temporary network problems, |
| 489 | and the existing module `${modulename}` may still work.') |
| 490 | if is_verbose { |
| 491 | eprintln('Details:') |
| 492 | eprintln(update_res.output) |
| 493 | } |
| 494 | eprintln('-'.repeat(50)) |
| 495 | } |
| 496 | } |
| 497 | return true |
| 498 | } |
| 499 | if is_verbose { |
| 500 | eprintln('check_module_is_installed: cloning from ${murl} ...') |
| 501 | } |
| 502 | cloning_res := |
| 503 | os.execute('${os.quoted_path(vexe)} retry -- git clone ${os.quoted_path(murl)} ${os.quoted_path(mpath)}') |
| 504 | if cloning_res.exit_code != 0 { |
| 505 | return error_with_code('cloning failed, details: ${cloning_res.output}', |
| 506 | cloning_res.exit_code) |
| 507 | } |
| 508 | if !os.exists(mod_v_file) { |
| 509 | return error('even after cloning, ${mod_v_file} is still missing') |
| 510 | } |
| 511 | if is_verbose { |
| 512 | eprintln('check_module_is_installed: done') |
| 513 | } |
| 514 | return true |
| 515 | } |
| 516 | |
| 517 | pub fn ensure_modules_for_all_tools_are_installed(is_verbose bool) { |
| 518 | for tool_name, tool_modules in external_module_dependencies_for_tool { |
| 519 | if is_verbose { |
| 520 | eprintln('Installing modules for tool: ${tool_name} ...') |
| 521 | } |
| 522 | for emodule in tool_modules { |
| 523 | check_module_is_installed(emodule, is_verbose, false) or { panic(err) } |
| 524 | } |
| 525 | } |
| 526 | } |
| 527 | |
| 528 | @[inline] |
| 529 | pub fn strip_mod_name(name string) string { |
| 530 | // For generic types like main.Message[main.Payload], strip module prefixes |
| 531 | // from both the type name and the generic parameters |
| 532 | if bracket_pos := name.index('[') { |
| 533 | prefix := name[..bracket_pos] |
| 534 | suffix := name[bracket_pos..] |
| 535 | // Also strip module names from generic parameters inside brackets |
| 536 | // e.g., [main.Payload, main.Foo] -> [Payload, Foo] |
| 537 | mut result := prefix.all_after_last('.') + '[' |
| 538 | params := suffix[1..suffix.len - 1] // Remove [ and ] |
| 539 | mut param_parts := []string{} |
| 540 | for param in params.split(', ') { |
| 541 | param_parts << param.all_after_last('.') |
| 542 | } |
| 543 | result += param_parts.join(', ') |
| 544 | result += ']' |
| 545 | return result |
| 546 | } |
| 547 | return name.all_after_last('.') |
| 548 | } |
| 549 | |
| 550 | @[inline] |
| 551 | pub fn strip_main_name(name string) string { |
| 552 | return name.replace('main.', '') |
| 553 | } |
| 554 | |
| 555 | @[inline] |
| 556 | pub fn no_dots(s string) string { |
| 557 | for ch in s { |
| 558 | if ch == `.` || ch == `-` { |
| 559 | return s.replace_each(['.', '__', '-', '_']) |
| 560 | } |
| 561 | } |
| 562 | return s |
| 563 | } |
| 564 | |
| 565 | const map_prefix = 'map[string]' |
| 566 | |
| 567 | // no_cur_mod - removes cur_mod. prefix from typename, |
| 568 | // but *only* when it is at the start, i.e.: |
| 569 | // no_cur_mod('vproto.Abdcdef', 'proto') == 'vproto.Abdcdef' |
| 570 | // even though proto. is a substring |
| 571 | pub fn no_cur_mod(typename string, cur_mod string) string { |
| 572 | mut res := typename |
| 573 | mod_prefix := cur_mod + '.' |
| 574 | has_map_prefix := res.starts_with(map_prefix) |
| 575 | if has_map_prefix { |
| 576 | res = res.replace_once(map_prefix, '') |
| 577 | } |
| 578 | no_symbols := res.trim_left('&[]') |
| 579 | should_shorten := no_symbols.starts_with(mod_prefix) |
| 580 | if should_shorten { |
| 581 | res = res.replace_once(mod_prefix, '') |
| 582 | } |
| 583 | if has_map_prefix { |
| 584 | res = map_prefix + res |
| 585 | } |
| 586 | return res |
| 587 | } |
| 588 | |
| 589 | pub fn prepare_tool_when_needed(source_name string) { |
| 590 | vexe := os.getenv('VEXE') |
| 591 | vroot := os.dir(vexe) |
| 592 | stool := os.join_path(vroot, 'cmd', 'tools', source_name) |
| 593 | tool_name, tool_exe := tool_source2name_and_exe(stool) |
| 594 | if should_recompile_tool(vexe, stool, tool_name, tool_exe) { |
| 595 | time.sleep((1001 + rand.intn(20) or { 0 }) * time.millisecond) // TODO: remove this when we can get mtime with a better resolution |
| 596 | recompile_file(vexe, stool) |
| 597 | } |
| 598 | } |
| 599 | |
| 600 | pub fn recompile_file(vexe string, file string) { |
| 601 | cmd := '${os.quoted_path(vexe)} ${os.quoted_path(file)}' |
| 602 | $if trace_recompilation ? { |
| 603 | println('recompilation command: ${cmd}') |
| 604 | } |
| 605 | recompile_result := os.system(cmd) |
| 606 | if recompile_result != 0 { |
| 607 | eprintln('could not recompile ${file}') |
| 608 | exit(2) |
| 609 | } |
| 610 | } |
| 611 | |
| 612 | // get_vtmp_folder returns the path to a folder, that is writable to V programs, |
| 613 | // and specific to the user. It can be overridden by setting the env variable `VTMP`. |
| 614 | pub fn get_vtmp_folder() string { |
| 615 | return os.vtmp_dir() |
| 616 | } |
| 617 | |
| 618 | pub fn should_bundle_module(mod string) bool { |
| 619 | return mod in bundle_modules || (mod.contains('.') && mod.all_before('.') in bundle_modules) |
| 620 | } |
| 621 | |
| 622 | // find_all_v_files - given a list of files/folders, finds all .v/.vsh files |
| 623 | // if some of the files/folders on the list does not exist, or a file is not |
| 624 | // a .v or .vsh file, returns an error instead. |
| 625 | pub fn find_all_v_files(roots []string) ![]string { |
| 626 | mut files := []string{} |
| 627 | for file in roots { |
| 628 | if os.is_dir(file) { |
| 629 | files << os.walk_ext(file, '.v') |
| 630 | files << os.walk_ext(file, '.vsh') |
| 631 | continue |
| 632 | } |
| 633 | if !file.ends_with('.v') && !file.ends_with('.vv') && !file.ends_with('.vsh') { |
| 634 | return error('v fmt can only be used on .v files.\nOffending file: "${file}"') |
| 635 | } |
| 636 | if !os.exists(file) { |
| 637 | return error('"${file}" does not exist') |
| 638 | } |
| 639 | files << file |
| 640 | } |
| 641 | return files |
| 642 | } |
| 643 | |
| 644 | // free_caches knows about all `util` caches and makes sure that they are freed |
| 645 | // if you add another cached unsafe function using static, do not forget to add |
| 646 | // a mechanism to clear its cache, and call it here. |
| 647 | pub fn free_caches() { |
| 648 | unsafe { |
| 649 | cached_file2sourcelines('') |
| 650 | cached_read_source_file('') or { '' } |
| 651 | } |
| 652 | } |
| 653 | |
| 654 | pub fn read_file(file_path string) !string { |
| 655 | return unsafe { cached_read_source_file(file_path) } |
| 656 | } |
| 657 | |