diff --git a/ds4.c b/ds4.c index d54ded8a..7b3134f7 100644 --- a/ds4.c +++ b/ds4.c @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,10 @@ #include "ds4.h" +#ifdef __APPLE__ +#include +#endif + #ifndef DS4_NO_GPU #include "ds4_gpu.h" #endif @@ -483,6 +488,87 @@ static char *ds4_strdup(const char *s) { return p; } +static bool ds4_path_is_absolute(const char *path) { + return path && path[0] == '/'; +} + +static char *ds4_executable_path(void) { +#ifdef __APPLE__ + uint32_t size = 0; + (void)_NSGetExecutablePath(NULL, &size); + if (size == 0) return NULL; + char *buf = xmalloc((size_t)size + 1); + if (_NSGetExecutablePath(buf, &size) != 0) { + free(buf); + return NULL; + } + buf[size] = '\0'; + char *resolved = realpath(buf, NULL); + if (resolved) { + free(buf); + return resolved; + } + return buf; +#elif defined(__linux__) +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif + char buf[PATH_MAX]; + ssize_t n = readlink("/proc/self/exe", buf, sizeof(buf) - 1); + if (n <= 0 || (size_t)n >= sizeof(buf) - 1) return NULL; + buf[n] = '\0'; + return ds4_strdup(buf); +#else + return NULL; +#endif +} + +static char *ds4_dirname_dup(const char *path) { + if (!path || !path[0]) return NULL; + const char *slash = strrchr(path, '/'); + if (!slash) return ds4_strdup("."); + size_t n = slash == path ? 1u : (size_t)(slash - path); + char *dir = xmalloc(n + 1); + memcpy(dir, path, n); + dir[n] = '\0'; + return dir; +} + +static char *ds4_join_path(const char *dir, const char *rel) { + size_t dir_len = strlen(dir); + size_t rel_len = strlen(rel); + bool need_slash = dir_len > 0 && dir[dir_len - 1] != '/'; + if (dir_len > SIZE_MAX - rel_len - (need_slash ? 2u : 1u)) { + ds4_die("path length overflow"); + } + char *out = xmalloc(dir_len + rel_len + (need_slash ? 2u : 1u)); + memcpy(out, dir, dir_len); + size_t pos = dir_len; + if (need_slash) out[pos++] = '/'; + memcpy(out + pos, rel, rel_len + 1); + return out; +} + +char *ds4_resolve_existing_path(const char *path) { + if (!path || !path[0]) return NULL; + if (ds4_path_is_absolute(path)) { + return access(path, F_OK) == 0 ? ds4_strdup(path) : NULL; + } + if (access(path, F_OK) == 0) return ds4_strdup(path); + + char *exe = ds4_executable_path(); + if (!exe) return NULL; + char *dir = ds4_dirname_dup(exe); + free(exe); + if (!dir) return NULL; + + char *candidate = ds4_join_path(dir, path); + free(dir); + if (access(candidate, F_OK) == 0) return candidate; + free(candidate); + return NULL; +} + static void *xrealloc(void *ptr, size_t size) { ds4_alloc_guard_check("realloc", size); void *p = realloc(ptr, size); @@ -1198,11 +1284,13 @@ static void model_open(ds4_model *m, const char *path, bool metal_mapping, memset(m, 0, sizeof(*m)); m->fd = -1; - int fd = open(path, O_RDONLY); - if (fd == -1) ds4_die_errno("cannot open model", path); + char *resolved_path = ds4_resolve_existing_path(path); + const char *open_path = resolved_path ? resolved_path : path; + int fd = open(open_path, O_RDONLY); + if (fd == -1) ds4_die_errno("cannot open model", open_path); struct stat st; - if (fstat(fd, &st) == -1) ds4_die_errno("cannot stat model", path); + if (fstat(fd, &st) == -1) ds4_die_errno("cannot stat model", open_path); if (st.st_size < 32) ds4_die("model file is too small to be GGUF"); /* @@ -1219,7 +1307,8 @@ static void model_open(ds4_model *m, const char *path, bool metal_mapping, */ const int mmap_flags = metal_mapping ? MAP_SHARED : MAP_PRIVATE; void *map = mmap(NULL, (size_t)st.st_size, PROT_READ, mmap_flags, fd, 0); - if (map == MAP_FAILED) ds4_die_errno("cannot mmap model", path); + if (map == MAP_FAILED) ds4_die_errno("cannot mmap model", open_path); + free(resolved_path); m->fd = fd; m->map = map; diff --git a/ds4.h b/ds4.h index 950d8dca..04bb9165 100644 --- a/ds4.h +++ b/ds4.h @@ -100,6 +100,9 @@ ds4_think_mode ds4_think_mode_for_context(ds4_think_mode mode, int ctx_size); ds4_context_memory ds4_context_memory_estimate(ds4_backend backend, int ctx_size); bool ds4_log_is_tty(FILE *fp); void ds4_log(FILE *fp, ds4_log_type type, const char *fmt, ...); +/* Return a malloc-owned existing path for PATH. Relative paths are resolved + * against the current directory first, then against the executable directory. */ +char *ds4_resolve_existing_path(const char *path); int ds4_engine_generate_argmax(ds4_engine *e, const ds4_tokens *prompt, int n_predict, int ctx_size, ds4_token_emit_fn emit, diff --git a/ds4_metal.m b/ds4_metal.m index 7ca42718..8d4c2dc1 100644 --- a/ds4_metal.m +++ b/ds4_metal.m @@ -1238,24 +1238,28 @@ void ds4_gpu_set_quality(bool quality) { NSString *loaded = nil; NSString *loaded_path = nil; for (NSString *path in paths) { - if (![fm fileExistsAtPath:path]) continue; + char *resolved_path = ds4_resolve_existing_path([path UTF8String]); + if (!resolved_path) continue; + NSString *read_path = [NSString stringWithUTF8String:resolved_path]; + free(resolved_path); + if (![fm fileExistsAtPath:read_path]) continue; NSError *error = nil; - loaded = [NSString stringWithContentsOfFile:path + loaded = [NSString stringWithContentsOfFile:read_path encoding:NSUTF8StringEncoding error:&error]; if (!loaded) { fprintf(stderr, "ds4: failed to read Metal source %s: %s\n", - [path UTF8String], [[error localizedDescription] UTF8String]); + [read_path UTF8String], [[error localizedDescription] UTF8String]); return nil; } - loaded_path = path; + loaded_path = read_path; break; } if (!loaded) { fprintf(stderr, - "ds4: Metal source %s not found (set %s to override)\n", + "ds4: Metal source %s not found relative to cwd or executable (set %s to override)\n", [spec[1] UTF8String], [spec[0] UTF8String]); return nil; } diff --git a/tests/ds4_test.c b/tests/ds4_test.c index 4bc4620d..2ab58a41 100644 --- a/tests/ds4_test.c +++ b/tests/ds4_test.c @@ -564,6 +564,52 @@ static void test_server_unit_group(void) { ds4_server_unit_tests_run(); } +static void test_binary_relative_path_resolution(void) { + char old_cwd[PATH_MAX] = {0}; + TEST_ASSERT(getcwd(old_cwd, sizeof(old_cwd)) != NULL); + if (!old_cwd[0]) return; + + char *repo_makefile = ds4_resolve_existing_path("Makefile"); + TEST_ASSERT(repo_makefile != NULL); + TEST_ASSERT(repo_makefile && access(repo_makefile, F_OK) == 0); + free(repo_makefile); + + char tmp_template[] = "/tmp/ds4-path-test-XXXXXX"; + char *tmpdir = mkdtemp(tmp_template); + TEST_ASSERT(tmpdir != NULL); + if (!tmpdir) return; + int rc = chdir(tmpdir); + TEST_ASSERT(rc == 0); + if (rc != 0) { + (void)rmdir(tmpdir); + return; + } + + const char *local_name = "ds4-local-path-test"; + int fd = open(local_name, O_WRONLY | O_CREAT | O_TRUNC, 0600); + TEST_ASSERT(fd >= 0); + if (fd >= 0) close(fd); + + char *local = ds4_resolve_existing_path(local_name); + TEST_ASSERT(local != NULL); + TEST_ASSERT(local && strcmp(local, local_name) == 0); + free(local); + + char *fallback = ds4_resolve_existing_path("Makefile"); + TEST_ASSERT(fallback != NULL); + TEST_ASSERT(fallback && fallback[0] == '/'); + TEST_ASSERT(fallback && access(fallback, F_OK) == 0); + free(fallback); + + char *missing = ds4_resolve_existing_path("ds4-missing-path-test"); + TEST_ASSERT(missing == NULL); + free(missing); + + TEST_ASSERT(unlink(local_name) == 0); + TEST_ASSERT(chdir(old_cwd) == 0); + TEST_ASSERT(rmdir(tmpdir) == 0); +} + typedef void (*test_fn)(void); typedef struct { @@ -581,6 +627,7 @@ static const ds4_test_entry test_entries[] = { {"--metal-kernels", "metal-kernels", "isolated Metal kernel numeric regressions", test_metal_f16_matvec_fast_nr0_4}, #endif {"--server", "server", "server parser/rendering/cache unit tests", test_server_unit_group}, + {"--paths", "paths", "binary-relative path resolution unit tests", test_binary_relative_path_resolution}, }; static void test_print_help(const char *prog) {