Browse Source

Merge pull request #4733 from laytan/get-executable-path

os/os2: add get_executable_path and get_executable_directory
gingerBill 6 months ago
parent
commit
3a13c598e2

+ 11 - 2
core/os/os2/errors_posix.odin

@@ -10,8 +10,12 @@ _error_string :: proc(errno: i32) -> string {
 	return string(posix.strerror(posix.Errno(errno)))
 }
 
-_get_platform_error :: proc() -> Error {
-	#partial switch errno := posix.errno(); errno {
+_get_platform_error_from_errno :: proc() -> Error {
+	return _get_platform_error_existing(posix.errno())
+}
+
+_get_platform_error_existing :: proc(errno: posix.Errno) -> Error {
+	#partial switch errno {
 	case .EPERM:
 		return .Permission_Denied
 	case .EEXIST:
@@ -32,3 +36,8 @@ _get_platform_error :: proc() -> Error {
 		return Platform_Error(errno)
 	}
 }
+
+_get_platform_error :: proc{
+	_get_platform_error_existing,
+	_get_platform_error_from_errno,
+}

+ 12 - 0
core/os/os2/path.odin

@@ -2,6 +2,8 @@ package os2
 
 import "base:runtime"
 
+import "core:path/filepath"
+
 Path_Separator        :: _Path_Separator        // OS-Specific
 Path_Separator_String :: _Path_Separator_String // OS-Specific
 Path_List_Separator   :: _Path_List_Separator   // OS-Specific
@@ -39,3 +41,13 @@ setwd :: set_working_directory
 set_working_directory :: proc(dir: string) -> (err: Error) {
 	return _set_working_directory(dir)
 }
+
+get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) {
+	return _get_executable_path(allocator)
+}
+
+get_executable_directory :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) {
+	path = _get_executable_path(allocator) or_return
+	path, _ = filepath.split(path)
+	return
+}

+ 17 - 0
core/os/os2/path_darwin.odin

@@ -0,0 +1,17 @@
+package os2
+
+import "base:runtime"
+
+import "core:sys/darwin"
+import "core:sys/posix"
+
+_get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) {
+	buffer: [darwin.PIDPATHINFO_MAXSIZE]byte = ---
+	ret := darwin.proc_pidpath(posix.getpid(), raw_data(buffer[:]), len(buffer))
+	if ret > 0 {
+		return clone_string(string(buffer[:ret]), allocator)
+	}
+
+	err = _get_platform_error()
+	return
+}

+ 29 - 0
core/os/os2/path_freebsd.odin

@@ -0,0 +1,29 @@
+package os2
+
+import "base:runtime"
+
+import "core:sys/freebsd"
+import "core:sys/posix"
+
+_get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) {
+	req := []freebsd.MIB_Identifier{.CTL_KERN, .KERN_PROC, .KERN_PROC_PATHNAME, freebsd.MIB_Identifier(-1)}
+
+	size: uint
+	if ret := freebsd.sysctl(req, nil, &size, nil, 0); ret != .NONE {
+		err = _get_platform_error(posix.Errno(ret))
+		return
+	}
+	assert(size > 0)
+
+	buf := make([]byte, size, allocator) or_return
+	defer if err != nil { delete(buf, allocator) }
+
+	assert(uint(len(buf)) == size)
+
+	if ret := freebsd.sysctl(req, raw_data(buf), &size, nil, 0); ret != .NONE {
+		err = _get_platform_error(posix.Errno(ret))
+		return
+	}
+
+	return string(buf[:size]), nil
+}

+ 21 - 1
core/os/os2/path_linux.odin

@@ -1,9 +1,10 @@
 #+private
 package os2
 
+import "base:runtime"
+
 import "core:strings"
 import "core:strconv"
-import "base:runtime"
 import "core:sys/linux"
 
 _Path_Separator        :: '/'
@@ -171,6 +172,25 @@ _set_working_directory :: proc(dir: string) -> Error {
 	return _get_platform_error(linux.chdir(dir_cstr))
 }
 
+_get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) {
+	TEMP_ALLOCATOR_GUARD()
+
+	buf := make([dynamic]byte, 1024, temp_allocator()) or_return
+	for {
+		n, errno := linux.readlink("/proc/self/exe", buf[:])
+		if errno != .NONE {
+			err = _get_platform_error(errno)
+			return
+		}
+
+		if n < len(buf) {
+			return clone_string(string(buf[:n]), allocator)
+		}
+
+		resize(&buf, len(buf)*2) or_return
+	}
+}
+
 _get_full_path :: proc(fd: linux.Fd, allocator: runtime.Allocator) -> (fullpath: string, err: Error) {
 	PROC_FD_PATH :: "/proc/self/fd/"
 

+ 24 - 0
core/os/os2/path_netbsd.odin

@@ -0,0 +1,24 @@
+package os2
+
+import "base:runtime"
+
+import "core:sys/posix"
+
+_get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) {
+	TEMP_ALLOCATOR_GUARD()
+
+	buf := make([dynamic]byte, 1024, temp_allocator()) or_return
+	for {
+		n := posix.readlink("/proc/curproc/exe", raw_data(buf), len(buf))
+		if n < 0 {
+			err = _get_platform_error()
+			return
+		}
+
+		if n < len(buf) {
+			return clone_string(string(buf[:n]), allocator)
+		}
+
+		resize(&buf, len(buf)*2) or_return
+	}
+}

+ 57 - 0
core/os/os2/path_openbsd.odin

@@ -0,0 +1,57 @@
+package os2
+
+import "base:runtime"
+
+import "core:strings"
+import "core:sys/posix"
+
+_get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) {
+	// OpenBSD does not have an API for this, we do our best below.
+
+	if len(runtime.args__) <= 0 {
+		err = .Invalid_Path
+		return
+	}
+
+	real :: proc(path: cstring, allocator: runtime.Allocator) -> (out: string, err: Error) {
+		real := posix.realpath(path)
+		if real == nil {
+			err = _get_platform_error()
+			return
+		}
+		defer posix.free(real)
+		return clone_string(string(real), allocator)
+	} 
+
+	arg := runtime.args__[0]
+	sarg := string(arg)
+
+	if len(sarg) == 0 {
+		err = .Invalid_Path
+		return
+	}
+
+	if sarg[0] == '.' || sarg[0] == '/' {
+		return real(arg, allocator)
+	}
+
+	TEMP_ALLOCATOR_GUARD()
+
+	buf := strings.builder_make(temp_allocator())
+
+	paths := get_env("PATH", temp_allocator())
+	for dir in strings.split_iterator(&paths, ":") {
+		strings.builder_reset(&buf)
+		strings.write_string(&buf, dir)
+		strings.write_string(&buf, "/")
+		strings.write_string(&buf, sarg)
+
+		cpath := strings.to_cstring(&buf)
+		if posix.access(cpath, {.X_OK}) == .OK {
+			return real(cpath, allocator)
+		}
+	}
+
+	err = .Invalid_Path
+	return
+}

+ 31 - 2
core/os/os2/path_wasi.odin

@@ -4,6 +4,7 @@ package os2
 import "base:runtime"
 
 import "core:path/filepath"
+import "core:sync"
 import "core:sys/wasm/wasi"
 
 _Path_Separator        :: '/'
@@ -74,11 +75,39 @@ _remove_all :: proc(path: string) -> (err: Error) {
 	return remove(path)
 }
 
+g_wd: string
+g_wd_mutex: sync.Mutex
+
 _get_working_directory :: proc(allocator: runtime.Allocator) -> (dir: string, err: Error) {
-	return ".", .Unsupported
+	sync.guard(&g_wd_mutex)
+
+	return clone_string(g_wd if g_wd != "" else "/", allocator)
 }
 
 _set_working_directory :: proc(dir: string) -> (err: Error) {
-	err = .Unsupported
+	sync.guard(&g_wd_mutex)
+
+	if dir == g_wd {
+		return
+	}
+
+	if g_wd != "" {
+		delete(g_wd, file_allocator())
+	}
+
+	g_wd = clone_string(dir, file_allocator()) or_return
 	return
 }
+
+_get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) {
+	if len(args) <= 0 {
+		return clone_string("/", allocator)
+	}
+
+	arg := args[0]
+	if len(arg) > 0 && (arg[0] == '.' || arg[0] == '/') {
+		return clone_string(arg, allocator)
+	}
+
+	return concatenate({"/", arg}, allocator)
+}

+ 20 - 0
core/os/os2/path_windows.odin

@@ -136,6 +136,26 @@ _set_working_directory :: proc(dir: string) -> (err: Error) {
 	return
 }
 
+_get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) {
+	TEMP_ALLOCATOR_GUARD()
+
+	buf := make([dynamic]u16, 512, temp_allocator()) or_return
+	for {
+		ret := win32.GetModuleFileNameW(nil, raw_data(buf), win32.DWORD(len(buf)))
+		if ret == 0 {
+			err = _get_platform_error()
+			return
+		}
+
+		if ret == win32.DWORD(len(buf)) && win32.GetLastError() == win32.ERROR_INSUFFICIENT_BUFFER {
+			resize(&buf, len(buf)*2) or_return
+			continue
+		}
+
+		return win32_utf16_to_utf8(buf[:ret], allocator)
+	}
+}
+
 can_use_long_paths: bool
 
 @(init)

+ 22 - 0
tests/core/os/os2/path.odin

@@ -0,0 +1,22 @@
+package tests_core_os_os2
+
+import os "core:os/os2"
+import    "core:log"
+import    "core:path/filepath"
+import    "core:testing"
+import    "core:strings"
+
+@(test)
+test_executable :: proc(t: ^testing.T) {
+	path, err := os.get_executable_path(context.allocator)
+	defer delete(path)
+
+	log.infof("executable path: %q", path)
+
+	// NOTE: some sanity checks that should always be the case, at least in the CI.
+
+	testing.expect_value(t, err, nil)
+	testing.expect(t, len(path) > 0)
+	testing.expect(t, filepath.is_abs(path))
+	testing.expectf(t, strings.contains(path, filepath.base(os.args[0])), "expected the executable path to contain the base of os.args[0] which is %q", filepath.base(os.args[0]))
+}