1 | // Copyright (c) 2019-2023 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 os |
5 | |
6 | import strings |
7 | |
8 | pub const max_path_len = 4096 |
9 | |
10 | pub const wd_at_startup = getwd() |
11 | |
12 | const f_ok = 0 |
13 | |
14 | const x_ok = 1 |
15 | |
16 | const w_ok = 2 |
17 | |
18 | const r_ok = 4 |
19 | |
20 | pub struct Result { |
21 | pub: |
22 | exit_code int |
23 | output string |
24 | // stderr string // TODO |
25 | } |
26 | |
27 | pub struct Command { |
28 | mut: |
29 | f voidptr |
30 | pub mut: |
31 | eof bool |
32 | exit_code int |
33 | pub: |
34 | path string |
35 | redirect_stdout bool |
36 | } |
37 | |
38 | [unsafe] |
39 | pub fn (mut result Result) free() { |
40 | unsafe { result.output.free() } |
41 | } |
42 | |
43 | // executable_fallback is used when there is not a more platform specific and accurate implementation. |
44 | // It relies on path manipulation of os.args[0] and os.wd_at_startup, so it may not work properly in |
45 | // all cases, but it should be better, than just using os.args[0] directly. |
46 | fn executable_fallback() string { |
47 | if args.len == 0 { |
48 | // we are early in the bootstrap, os.args has not been initialized yet :-| |
49 | return '' |
50 | } |
51 | mut exepath := args[0] |
52 | $if windows { |
53 | if !exepath.contains('.exe') { |
54 | exepath += '.exe' |
55 | } |
56 | } |
57 | if !is_abs_path(exepath) { |
58 | other_seperator := if path_separator == '/' { '\\' } else { '/' } |
59 | rexepath := exepath.replace(other_seperator, path_separator) |
60 | if rexepath.contains(path_separator) { |
61 | exepath = join_path_single(os.wd_at_startup, exepath) |
62 | } else { |
63 | // no choice but to try to walk the PATH folders :-| ... |
64 | foundpath := find_abs_path_of_executable(exepath) or { '' } |
65 | if foundpath.len > 0 { |
66 | exepath = foundpath |
67 | } |
68 | } |
69 | } |
70 | exepath = real_path(exepath) |
71 | return exepath |
72 | } |
73 | |
74 | // cp_all will recursively copy `src` to `dst`, |
75 | // optionally overwriting files or dirs in `dst`. |
76 | pub fn cp_all(src string, dst string, overwrite bool) ! { |
77 | source_path := real_path(src) |
78 | dest_path := real_path(dst) |
79 | if !exists(source_path) { |
80 | return error("Source path doesn't exist") |
81 | } |
82 | // single file copy |
83 | if !is_dir(source_path) { |
84 | fname := file_name(source_path) |
85 | adjusted_path := if is_dir(dest_path) { |
86 | join_path_single(dest_path, fname) |
87 | } else { |
88 | dest_path |
89 | } |
90 | if exists(adjusted_path) { |
91 | if overwrite { |
92 | rm(adjusted_path)! |
93 | } else { |
94 | return error('Destination file path already exist') |
95 | } |
96 | } |
97 | cp(source_path, adjusted_path)! |
98 | return |
99 | } |
100 | if !exists(dest_path) { |
101 | mkdir(dest_path)! |
102 | } |
103 | if !is_dir(dest_path) { |
104 | return error('Destination path is not a valid directory') |
105 | } |
106 | files := ls(source_path)! |
107 | for file in files { |
108 | sp := join_path_single(source_path, file) |
109 | dp := join_path_single(dest_path, file) |
110 | if is_dir(sp) { |
111 | if !exists(dp) { |
112 | mkdir(dp)! |
113 | } |
114 | } |
115 | cp_all(sp, dp, overwrite) or { |
116 | rmdir(dp) or { return err } |
117 | return err |
118 | } |
119 | } |
120 | } |
121 | |
122 | // mv_by_cp first copies the source file, and if it is copied successfully, deletes the source file. |
123 | // may be used when you are not sure that the source and target are on the same mount/partition. |
124 | pub fn mv_by_cp(source string, target string) ! { |
125 | cp(source, target)! |
126 | rm(source)! |
127 | } |
128 | |
129 | // mv moves files or folders from `src` to `dst`. |
130 | pub fn mv(source string, target string) ! { |
131 | rename(source, target) or { mv_by_cp(source, target)! } |
132 | } |
133 | |
134 | // read_lines reads the file in `path` into an array of lines. |
135 | [manualfree] |
136 | pub fn read_lines(path string) ![]string { |
137 | buf := read_file(path)! |
138 | res := buf.split_into_lines() |
139 | unsafe { buf.free() } |
140 | return res |
141 | } |
142 | |
143 | // sigint_to_signal_name will translate `si` signal integer code to it's string code representation. |
144 | pub fn sigint_to_signal_name(si int) string { |
145 | // POSIX signals: |
146 | match si { |
147 | 1 { return 'SIGHUP' } |
148 | 2 { return 'SIGINT' } |
149 | 3 { return 'SIGQUIT' } |
150 | 4 { return 'SIGILL' } |
151 | 6 { return 'SIGABRT' } |
152 | 8 { return 'SIGFPE' } |
153 | 9 { return 'SIGKILL' } |
154 | 11 { return 'SIGSEGV' } |
155 | 13 { return 'SIGPIPE' } |
156 | 14 { return 'SIGALRM' } |
157 | 15 { return 'SIGTERM' } |
158 | else {} |
159 | } |
160 | $if linux { |
161 | // From `man 7 signal` on linux: |
162 | match si { |
163 | // TODO dependent on platform |
164 | // works only on x86/ARM/most others |
165 | 10 { // , 30, 16 |
166 | return 'SIGUSR1' |
167 | } |
168 | 12 { // , 31, 17 |
169 | return 'SIGUSR2' |
170 | } |
171 | 17 { // , 20, 18 |
172 | return 'SIGCHLD' |
173 | } |
174 | 18 { // , 19, 25 |
175 | return 'SIGCONT' |
176 | } |
177 | 19 { // , 17, 23 |
178 | return 'SIGSTOP' |
179 | } |
180 | 20 { // , 18, 24 |
181 | return 'SIGTSTP' |
182 | } |
183 | 21 { // , 26 |
184 | return 'SIGTTIN' |
185 | } |
186 | 22 { // , 27 |
187 | return 'SIGTTOU' |
188 | } |
189 | // ///////////////////////////// |
190 | 5 { |
191 | return 'SIGTRAP' |
192 | } |
193 | 7 { |
194 | return 'SIGBUS' |
195 | } |
196 | else {} |
197 | } |
198 | } |
199 | return 'unknown' |
200 | } |
201 | |
202 | // rmdir_all recursively removes the specified directory. |
203 | pub fn rmdir_all(path string) ! { |
204 | mut ret_err := '' |
205 | items := ls(path)! |
206 | for item in items { |
207 | fullpath := join_path_single(path, item) |
208 | if is_dir(fullpath) && !is_link(fullpath) { |
209 | rmdir_all(fullpath) or { ret_err = err.msg() } |
210 | } else { |
211 | rm(fullpath) or { ret_err = err.msg() } |
212 | } |
213 | } |
214 | rmdir(path) or { ret_err = err.msg() } |
215 | if ret_err.len > 0 { |
216 | return error(ret_err) |
217 | } |
218 | } |
219 | |
220 | // is_dir_empty will return a `bool` whether or not `path` is empty. |
221 | // Note that it will return `true` if `path` does not exist. |
222 | [manualfree] |
223 | pub fn is_dir_empty(path string) bool { |
224 | items := ls(path) or { return true } |
225 | res := items.len == 0 |
226 | unsafe { items.free() } |
227 | return res |
228 | } |
229 | |
230 | // file_ext will return the part after the last occurence of `.` in `path`. |
231 | // The `.` is included. |
232 | // Examples: |
233 | // ```v |
234 | // assert os.file_ext('file.v') == '.v' |
235 | // assert os.file_ext('.ignore_me') == '' |
236 | // assert os.file_ext('.') == '' |
237 | // ``` |
238 | pub fn file_ext(opath string) string { |
239 | if opath.len < 3 { |
240 | return empty_str |
241 | } |
242 | path := file_name(opath) |
243 | pos := path.last_index(dot_str) or { return empty_str } |
244 | if pos + 1 >= path.len || pos == 0 { |
245 | return empty_str |
246 | } |
247 | return path[pos..] |
248 | } |
249 | |
250 | // dir returns all but the last element of path, typically the path's directory. |
251 | // After dropping the final element, trailing slashes are removed. |
252 | // If the path is empty, dir returns ".". If the path consists entirely of separators, |
253 | // dir returns a single separator. |
254 | // The returned path does not end in a separator unless it is the root directory. |
255 | pub fn dir(opath string) string { |
256 | if opath == '' { |
257 | return '.' |
258 | } |
259 | other_seperator := if path_separator == '/' { '\\' } else { '/' } |
260 | path := opath.replace(other_seperator, path_separator) |
261 | pos := path.last_index(path_separator) or { return '.' } |
262 | if pos == 0 && path_separator == '/' { |
263 | return '/' |
264 | } |
265 | return path[..pos] |
266 | } |
267 | |
268 | // base returns the last element of path. |
269 | // Trailing path separators are removed before extracting the last element. |
270 | // If the path is empty, base returns ".". If the path consists entirely of separators, base returns a |
271 | // single separator. |
272 | pub fn base(opath string) string { |
273 | if opath == '' { |
274 | return '.' |
275 | } |
276 | other_seperator := if path_separator == '/' { '\\' } else { '/' } |
277 | path := opath.replace(other_seperator, path_separator) |
278 | if path == path_separator { |
279 | return path_separator |
280 | } |
281 | if path.ends_with(path_separator) { |
282 | path2 := path[..path.len - 1] |
283 | pos := path2.last_index(path_separator) or { return path2.clone() } |
284 | return path2[pos + 1..] |
285 | } |
286 | pos := path.last_index(path_separator) or { return path.clone() } |
287 | return path[pos + 1..] |
288 | } |
289 | |
290 | // file_name will return all characters found after the last occurence of `path_separator`. |
291 | // file extension is included. |
292 | pub fn file_name(opath string) string { |
293 | other_seperator := if path_separator == '/' { '\\' } else { '/' } |
294 | path := opath.replace(other_seperator, path_separator) |
295 | return path.all_after_last(path_separator) |
296 | } |
297 | |
298 | // input_opt returns a one-line string from stdin, after printing a prompt. |
299 | // Returns `none` in case of an error (end of input). |
300 | pub fn input_opt(prompt string) ?string { |
301 | print(prompt) |
302 | flush() |
303 | res := get_raw_line() |
304 | if res.len > 0 { |
305 | return res.trim_right('\r\n') |
306 | } |
307 | return none |
308 | } |
309 | |
310 | // input returns a one-line string from stdin, after printing a prompt. |
311 | // Returns '<EOF>' in case of an error (end of input). |
312 | pub fn input(prompt string) string { |
313 | res := input_opt(prompt) or { return '<EOF>' } |
314 | return res |
315 | } |
316 | |
317 | // get_line returns a one-line string from stdin |
318 | pub fn get_line() string { |
319 | str := get_raw_line() |
320 | $if windows { |
321 | return str.trim_right('\r\n') |
322 | } |
323 | return str.trim_right('\n') |
324 | } |
325 | |
326 | // get_lines returns an array of strings read from from stdin. |
327 | // reading is stopped when an empty line is read. |
328 | pub fn get_lines() []string { |
329 | mut line := '' |
330 | mut inputstr := []string{} |
331 | for { |
332 | line = get_line() |
333 | if line.len <= 0 { |
334 | break |
335 | } |
336 | line = line.trim_space() |
337 | inputstr << line |
338 | } |
339 | return inputstr |
340 | } |
341 | |
342 | // get_lines_joined returns a string of the values read from from stdin. |
343 | // reading is stopped when an empty line is read. |
344 | pub fn get_lines_joined() string { |
345 | mut line := '' |
346 | mut inputstr := '' |
347 | for { |
348 | line = get_line() |
349 | if line.len <= 0 { |
350 | break |
351 | } |
352 | line = line.trim_space() |
353 | inputstr += line |
354 | } |
355 | return inputstr |
356 | } |
357 | |
358 | // get_raw_lines_joined reads *all* input lines from stdin. |
359 | // It returns them as one large string. Note: unlike os.get_lines_joined, |
360 | // empty lines (that contain only `\r\n` or `\n`), will be present in |
361 | // the output. |
362 | // Reading is stopped, only on EOF of stdin. |
363 | pub fn get_raw_lines_joined() string { |
364 | mut line := '' |
365 | mut lines := []string{} |
366 | for { |
367 | line = get_raw_line() |
368 | if line.len <= 0 { |
369 | break |
370 | } |
371 | lines << line |
372 | } |
373 | res := lines.join('') |
374 | return res |
375 | } |
376 | |
377 | // user_os returns current user operating system name. |
378 | pub fn user_os() string { |
379 | $if linux { |
380 | return 'linux' |
381 | } |
382 | $if macos { |
383 | return 'macos' |
384 | } |
385 | $if windows { |
386 | return 'windows' |
387 | } |
388 | $if freebsd { |
389 | return 'freebsd' |
390 | } |
391 | $if openbsd { |
392 | return 'openbsd' |
393 | } |
394 | $if netbsd { |
395 | return 'netbsd' |
396 | } |
397 | $if dragonfly { |
398 | return 'dragonfly' |
399 | } |
400 | $if android { |
401 | return 'android' |
402 | } |
403 | $if termux { |
404 | return 'termux' |
405 | } |
406 | $if solaris { |
407 | return 'solaris' |
408 | } |
409 | $if qnx { |
410 | return 'qnx' |
411 | } |
412 | $if haiku { |
413 | return 'haiku' |
414 | } |
415 | $if serenity { |
416 | return 'serenity' |
417 | } |
418 | $if vinix { |
419 | return 'vinix' |
420 | } |
421 | if getenv('TERMUX_VERSION') != '' { |
422 | return 'termux' |
423 | } |
424 | return 'unknown' |
425 | } |
426 | |
427 | // user_names returns an array of the name of every user on the system. |
428 | pub fn user_names() ![]string { |
429 | $if windows { |
430 | result := execute('wmic useraccount get name') |
431 | if result.exit_code != 0 { |
432 | return error('Failed to get user names. Exited with code ${result.exit_code}: ${result.output}') |
433 | } |
434 | mut users := result.output.split_into_lines() |
435 | // windows command prints an empty line at the end of output |
436 | users.delete(users.len - 1) |
437 | return users |
438 | } $else { |
439 | lines := read_lines('/etc/passwd')! |
440 | mut users := []string{cap: lines.len} |
441 | for line in lines { |
442 | end_name := line.index(':') or { line.len } |
443 | users << line[0..end_name] |
444 | } |
445 | return users |
446 | } |
447 | } |
448 | |
449 | // home_dir returns path to the user's home directory. |
450 | pub fn home_dir() string { |
451 | $if windows { |
452 | return getenv('USERPROFILE') |
453 | } $else { |
454 | // println('home_dir() call') |
455 | // res:= os.getenv('HOME') |
456 | // println('res="$res"') |
457 | return getenv('HOME') |
458 | } |
459 | } |
460 | |
461 | // expand_tilde_to_home expands the character `~` in `path` to the user's home directory. |
462 | // See also `home_dir()`. |
463 | pub fn expand_tilde_to_home(path string) string { |
464 | if path == '~' { |
465 | return home_dir().trim_right(path_separator) |
466 | } |
467 | if path.starts_with('~' + path_separator) { |
468 | return path.replace_once('~' + path_separator, home_dir().trim_right(path_separator) + |
469 | path_separator) |
470 | } |
471 | return path |
472 | } |
473 | |
474 | // write_file writes `text` data to the file in `path`. |
475 | // If `path` exists, the contents of `path` will be overwritten with the contents of `text`. |
476 | pub fn write_file(path string, text string) ! { |
477 | mut f := create(path)! |
478 | unsafe { f.write_full_buffer(text.str, usize(text.len))! } |
479 | f.close() |
480 | } |
481 | |
482 | pub struct ExecutableNotFoundError { |
483 | Error |
484 | } |
485 | |
486 | pub fn (err ExecutableNotFoundError) msg() string { |
487 | return 'os: failed to find executable' |
488 | } |
489 | |
490 | fn error_failed_to_find_executable() IError { |
491 | return &ExecutableNotFoundError{} |
492 | } |
493 | |
494 | // find_abs_path_of_executable walks the environment PATH, just like most shell do, it returns |
495 | // the absolute path of the executable if found |
496 | pub fn find_abs_path_of_executable(exepath string) !string { |
497 | if exepath == '' { |
498 | return error('expected non empty `exepath`') |
499 | } |
500 | |
501 | for suffix in executable_suffixes { |
502 | fexepath := exepath + suffix |
503 | if is_abs_path(fexepath) { |
504 | return real_path(fexepath) |
505 | } |
506 | mut res := '' |
507 | path := getenv('PATH') |
508 | paths := path.split(path_delimiter) |
509 | for p in paths { |
510 | found_abs_path := join_path_single(p, fexepath) |
511 | $if trace_find_abs_path_of_executable ? { |
512 | dump(found_abs_path) |
513 | } |
514 | if exists(found_abs_path) && is_executable(found_abs_path) { |
515 | res = found_abs_path |
516 | break |
517 | } |
518 | } |
519 | if res.len > 0 { |
520 | return real_path(res) |
521 | } |
522 | } |
523 | return error_failed_to_find_executable() |
524 | } |
525 | |
526 | // exists_in_system_path returns `true` if `prog` exists in the system's PATH |
527 | pub fn exists_in_system_path(prog string) bool { |
528 | find_abs_path_of_executable(prog) or { return false } |
529 | return true |
530 | } |
531 | |
532 | // is_file returns a `bool` indicating whether the given `path` is a file. |
533 | pub fn is_file(path string) bool { |
534 | return exists(path) && !is_dir(path) |
535 | } |
536 | |
537 | // join_path returns a path as string from input string parameter(s). |
538 | [manualfree] |
539 | pub fn join_path(base string, dirs ...string) string { |
540 | // TODO: fix freeing of `dirs` when the passed arguments are variadic, |
541 | // but do not free the arr, when `os.join_path(base, ...arr)` is called. |
542 | mut sb := strings.new_builder(base.len + dirs.len * 50) |
543 | defer { |
544 | unsafe { sb.free() } |
545 | } |
546 | sbase := base.trim_right('\\/') |
547 | defer { |
548 | unsafe { sbase.free() } |
549 | } |
550 | sb.write_string(sbase) |
551 | for d in dirs { |
552 | sb.write_string(path_separator) |
553 | sb.write_string(d) |
554 | } |
555 | mut res := sb.str() |
556 | if res.contains('/./') { |
557 | // Fix `join_path("/foo/bar", "./file.txt")` => `/foo/bar/./file.txt` |
558 | res = res.replace('/./', '/') |
559 | } |
560 | return res |
561 | } |
562 | |
563 | // join_path_single appends the `elem` after `base`, using a platform specific |
564 | // path_separator. |
565 | [manualfree] |
566 | pub fn join_path_single(base string, elem string) string { |
567 | // TODO: deprecate this and make it `return os.join_path(base, elem)`, |
568 | // when freeing variadic args vs ...arr is solved in the compiler |
569 | mut sb := strings.new_builder(base.len + elem.len + 1) |
570 | defer { |
571 | unsafe { sb.free() } |
572 | } |
573 | sbase := base.trim_right('\\/') |
574 | defer { |
575 | unsafe { sbase.free() } |
576 | } |
577 | sb.write_string(sbase) |
578 | sb.write_string(path_separator) |
579 | sb.write_string(elem) |
580 | return sb.str() |
581 | } |
582 | |
583 | // walk_ext returns a recursive list of all files in `path` ending with `ext`. |
584 | pub fn walk_ext(path string, ext string) []string { |
585 | mut res := []string{} |
586 | impl_walk_ext(path, ext, mut res) |
587 | return res |
588 | } |
589 | |
590 | fn impl_walk_ext(path string, ext string, mut out []string) { |
591 | if !is_dir(path) { |
592 | return |
593 | } |
594 | mut files := ls(path) or { return } |
595 | separator := if path.ends_with(path_separator) { '' } else { path_separator } |
596 | for file in files { |
597 | if file.starts_with('.') { |
598 | continue |
599 | } |
600 | p := path + separator + file |
601 | if is_dir(p) && !is_link(p) { |
602 | impl_walk_ext(p, ext, mut out) |
603 | } else if file.ends_with(ext) { |
604 | out << p |
605 | } |
606 | } |
607 | } |
608 | |
609 | // walk traverses the given directory `path`. |
610 | // When a file is encountred, it will call the callback `f` with current file as argument. |
611 | // Note: walk can be called even for deeply nested folders, |
612 | // since it does not recurse, but processes them iteratively. |
613 | pub fn walk(path string, f fn (string)) { |
614 | if path.len == 0 { |
615 | return |
616 | } |
617 | if !is_dir(path) { |
618 | return |
619 | } |
620 | mut remaining := []string{cap: 1000} |
621 | clean_path := path.trim_right(path_separator) |
622 | $if windows { |
623 | remaining << clean_path.replace('/', '\\') |
624 | } $else { |
625 | remaining << clean_path |
626 | } |
627 | for remaining.len > 0 { |
628 | cpath := remaining.pop() |
629 | pkind := kind_of_existing_path(cpath) |
630 | if pkind.is_link || !pkind.is_dir { |
631 | f(cpath) |
632 | continue |
633 | } |
634 | mut files := ls(cpath) or { continue } |
635 | for idx := files.len - 1; idx >= 0; idx-- { |
636 | remaining << cpath + path_separator + files[idx] |
637 | } |
638 | } |
639 | } |
640 | |
641 | // FnWalkContextCB is used to define the callback functions, passed to os.walk_context |
642 | pub type FnWalkContextCB = fn (voidptr, string) |
643 | |
644 | // walk_with_context traverses the given directory `path`. |
645 | // For each encountred file *and* directory, it will call your `fcb` callback, |
646 | // passing it the arbitrary `context` in its first parameter, |
647 | // and the path to the file in its second parameter. |
648 | // Note: walk_with_context can be called even for deeply nested folders, |
649 | // since it does not recurse, but processes them iteratively. |
650 | pub fn walk_with_context(path string, context voidptr, fcb FnWalkContextCB) { |
651 | if path.len == 0 { |
652 | return |
653 | } |
654 | if !is_dir(path) { |
655 | return |
656 | } |
657 | mut remaining := []string{cap: 1000} |
658 | clean_path := path.trim_right(path_separator) |
659 | $if windows { |
660 | remaining << clean_path.replace('/', '\\') |
661 | } $else { |
662 | remaining << clean_path |
663 | } |
664 | mut loops := 0 |
665 | for remaining.len > 0 { |
666 | loops++ |
667 | cpath := remaining.pop() |
668 | // call `fcb` for everything, but the initial folder: |
669 | if loops > 1 { |
670 | fcb(context, cpath) |
671 | } |
672 | pkind := kind_of_existing_path(cpath) |
673 | if pkind.is_link || !pkind.is_dir { |
674 | continue |
675 | } |
676 | mut files := ls(cpath) or { continue } |
677 | for idx := files.len - 1; idx >= 0; idx-- { |
678 | remaining << cpath + path_separator + files[idx] |
679 | } |
680 | } |
681 | } |
682 | |
683 | // log will print "os.log: "+`s` ... |
684 | pub fn log(s string) { |
685 | println('os.log: ' + s) |
686 | } |
687 | |
688 | [params] |
689 | pub struct MkdirParams { |
690 | mode u32 = 0o777 // note that the actual mode is affected by the process's umask |
691 | } |
692 | |
693 | // mkdir_all will create a valid full path of all directories given in `path`. |
694 | pub fn mkdir_all(opath string, params MkdirParams) ! { |
695 | other_seperator := if path_separator == '/' { '\\' } else { '/' } |
696 | path := opath.replace(other_seperator, path_separator) |
697 | mut p := if path.starts_with(path_separator) { path_separator } else { '' } |
698 | path_parts := path.trim_left(path_separator).split(path_separator) |
699 | for subdir in path_parts { |
700 | p += subdir + path_separator |
701 | if exists(p) && is_dir(p) { |
702 | continue |
703 | } |
704 | mkdir(p, params) or { return error('folder: ${p}, error: ${err}') } |
705 | } |
706 | } |
707 | |
708 | // cache_dir returns the path to a *writable* user specific folder, suitable for writing non-essential data. |
709 | pub fn cache_dir() string { |
710 | // See: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html |
711 | // There is a single base directory relative to which user-specific non-essential |
712 | // (cached) data should be written. This directory is defined by the environment |
713 | // variable $XDG_CACHE_HOME. |
714 | // $XDG_CACHE_HOME defines the base directory relative to which user specific |
715 | // non-essential data files should be stored. If $XDG_CACHE_HOME is either not set |
716 | // or empty, a default equal to $HOME/.cache should be used. |
717 | xdg_cache_home := getenv('XDG_CACHE_HOME') |
718 | if xdg_cache_home != '' { |
719 | return xdg_cache_home |
720 | } |
721 | cdir := join_path_single(home_dir(), '.cache') |
722 | if !is_dir(cdir) && !is_link(cdir) { |
723 | mkdir(cdir) or { panic(err) } |
724 | } |
725 | return cdir |
726 | } |
727 | |
728 | // temp_dir returns the path to a folder, that is suitable for storing temporary files. |
729 | pub fn temp_dir() string { |
730 | mut path := getenv('TMPDIR') |
731 | $if windows { |
732 | if path == '' { |
733 | // TODO see Qt's implementation? |
734 | // https://doc.qt.io/qt-5/qdir.html#tempPath |
735 | // https://github.com/qt/qtbase/blob/e164d61ca8263fc4b46fdd916e1ea77c7dd2b735/src/corelib/io/qfilesystemengine_win.cpp#L1275 |
736 | path = getenv('TEMP') |
737 | if path == '' { |
738 | path = getenv('TMP') |
739 | } |
740 | if path == '' { |
741 | path = 'C:/tmp' |
742 | } |
743 | } |
744 | path = get_long_path(path) or { path } |
745 | } |
746 | $if macos { |
747 | // avoid /var/folders/6j/cmsk8gd90pd.... on macs |
748 | return '/tmp' |
749 | } |
750 | $if android { |
751 | // TODO test+use '/data/local/tmp' on Android before using cache_dir() |
752 | if path == '' { |
753 | path = cache_dir() |
754 | } |
755 | } |
756 | $if termux { |
757 | path = '/data/data/com.termux/files/usr/tmp' |
758 | } |
759 | if path == '' { |
760 | path = '/tmp' |
761 | } |
762 | return path |
763 | } |
764 | |
765 | // vtmp_dir returns the path to a folder, that is writable to V programs, *and* specific |
766 | // to the OS user. It can be overridden by setting the env variable `VTMP`. |
767 | pub fn vtmp_dir() string { |
768 | mut vtmp := getenv('VTMP') |
769 | if vtmp.len > 0 { |
770 | return vtmp |
771 | } |
772 | uid := getuid() |
773 | vtmp = join_path_single(temp_dir(), 'v_${uid}') |
774 | if !exists(vtmp) || !is_dir(vtmp) { |
775 | // create a new directory, that is private to the user: |
776 | mkdir_all(vtmp, mode: 0o700) or { panic(err) } |
777 | } |
778 | setenv('VTMP', vtmp, true) |
779 | return vtmp |
780 | } |
781 | |
782 | fn default_vmodules_path() string { |
783 | hdir := home_dir() |
784 | res := join_path_single(hdir, '.vmodules') |
785 | return res |
786 | } |
787 | |
788 | // vmodules_dir returns the path to a folder, where v stores its global modules. |
789 | pub fn vmodules_dir() string { |
790 | paths := vmodules_paths() |
791 | if paths.len > 0 { |
792 | return paths[0] |
793 | } |
794 | return default_vmodules_path() |
795 | } |
796 | |
797 | // vmodules_paths returns a list of paths, where v looks up for modules. |
798 | // You can customize it through setting the environment variable VMODULES |
799 | // [manualfree] |
800 | pub fn vmodules_paths() []string { |
801 | mut path := getenv('VMODULES') |
802 | if path == '' { |
803 | // unsafe { path.free() } |
804 | path = default_vmodules_path() |
805 | } |
806 | defer { |
807 | // unsafe { path.free() } |
808 | } |
809 | splitted := path.split(path_delimiter) |
810 | defer { |
811 | // unsafe { splitted.free() } |
812 | } |
813 | mut list := []string{cap: splitted.len} |
814 | for i in 0 .. splitted.len { |
815 | si := splitted[i] |
816 | trimmed := si.trim_right(path_separator) |
817 | list << trimmed |
818 | // unsafe { trimmed.free() } |
819 | // unsafe { si.free() } |
820 | } |
821 | return list |
822 | } |
823 | |
824 | // resource_abs_path returns an absolute path, for the given `path`. |
825 | // (the path is expected to be relative to the executable program) |
826 | // See https://discordapp.com/channels/592103645835821068/592294828432424960/630806741373943808 |
827 | // It gives a convenient way to access program resources like images, fonts, sounds and so on, |
828 | // *no matter* how the program was started, and what is the current working directory. |
829 | [manualfree] |
830 | pub fn resource_abs_path(path string) string { |
831 | exe := executable() |
832 | dexe := dir(exe) |
833 | mut base_path := real_path(dexe) |
834 | vresource := getenv('V_RESOURCE_PATH') |
835 | if vresource.len != 0 { |
836 | unsafe { base_path.free() } |
837 | base_path = vresource |
838 | } |
839 | fp := join_path_single(base_path, path) |
840 | res := real_path(fp) |
841 | unsafe { |
842 | fp.free() |
843 | vresource.free() |
844 | base_path.free() |
845 | dexe.free() |
846 | exe.free() |
847 | } |
848 | return res |
849 | } |
850 | |
851 | pub struct Uname { |
852 | pub mut: |
853 | sysname string |
854 | nodename string |
855 | release string |
856 | version string |
857 | machine string |
858 | } |
859 | |
860 | pub fn execute_or_panic(cmd string) Result { |
861 | res := execute(cmd) |
862 | if res.exit_code != 0 { |
863 | eprintln('failed cmd: ${cmd}') |
864 | eprintln('failed code: ${res.exit_code}') |
865 | panic(res.output) |
866 | } |
867 | return res |
868 | } |
869 | |
870 | pub fn execute_or_exit(cmd string) Result { |
871 | res := execute(cmd) |
872 | if res.exit_code != 0 { |
873 | eprintln('failed cmd: ${cmd}') |
874 | eprintln('failed code: ${res.exit_code}') |
875 | eprintln(res.output) |
876 | exit(1) |
877 | } |
878 | return res |
879 | } |
880 | |
881 | // quoted path - return a quoted version of the path, depending on the platform. |
882 | pub fn quoted_path(path string) string { |
883 | $if windows { |
884 | return if path.ends_with(path_separator) { |
885 | '"${path + path_separator}"' |
886 | } else { |
887 | '"${path}"' |
888 | } |
889 | } $else { |
890 | return "'${path}'" |
891 | } |
892 | } |
893 | |
894 | // config_dir returns the path to the user configuration directory (depending on the platform). |
895 | // On windows, that is `%AppData%`. |
896 | // On macos, that is `~/Library/Application Support`. |
897 | // On the rest, that is `$XDG_CONFIG_HOME`, or if that is not available, `~/.config`. |
898 | // If the path cannot be determined, it returns an error. |
899 | // (for example, when $HOME on linux, or %AppData% on windows is not defined) |
900 | pub fn config_dir() !string { |
901 | $if windows { |
902 | app_data := getenv('AppData') |
903 | if app_data != '' { |
904 | return app_data |
905 | } |
906 | } $else $if macos || darwin || ios { |
907 | home := home_dir() |
908 | if home != '' { |
909 | return home + '/Library/Application Support' |
910 | } |
911 | } $else { |
912 | xdg_home := getenv('XDG_CONFIG_HOME') |
913 | if xdg_home != '' { |
914 | return xdg_home |
915 | } |
916 | home := home_dir() |
917 | if home != '' { |
918 | return home + '/.config' |
919 | } |
920 | } |
921 | return error('Cannot find config directory') |
922 | } |