SDL: Added SDL_PROP_PROCESS_CREATE_WORKING_DIRECTORY_STRING

From 0f27c3aabd07364bbe87f16d56984c605239008f Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Thu, 27 Mar 2025 08:17:49 -0700
Subject: [PATCH] Added SDL_PROP_PROCESS_CREATE_WORKING_DIRECTORY_STRING

Fixes https://github.com/libsdl-org/SDL/issues/12654
---
 CMakeLists.txt                                |  2 ++
 include/SDL3/SDL_process.h                    |  2 ++
 include/build_config/SDL_build_config.h.cmake |  2 ++
 src/process/posix/SDL_posixprocess.c          | 31 +++++++++++++++++++
 src/process/windows/SDL_windowsprocess.c      | 12 ++++++-
 5 files changed, 48 insertions(+), 1 deletion(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 03f7e46ff7c7f..4602e09996937 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1104,6 +1104,8 @@ if(SDL_LIBC)
     check_symbol_exists(poll "poll.h" HAVE_POLL)
     check_symbol_exists(memfd_create "sys/mman.h" HAVE_MEMFD_CREATE)
     check_symbol_exists(posix_fallocate "fcntl.h" HAVE_POSIX_FALLOCATE)
+    check_symbol_exists(posix_spawn_file_actions_addchdir "spawn.h" HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR)
+    check_symbol_exists(posix_spawn_file_actions_addchdir_np "spawn.h" HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP)
 
     if(SDL_SYSTEM_ICONV)
       check_c_source_compiles("
diff --git a/include/SDL3/SDL_process.h b/include/SDL3/SDL_process.h
index 511b2f9c51f96..870697bd7a7af 100644
--- a/include/SDL3/SDL_process.h
+++ b/include/SDL3/SDL_process.h
@@ -166,6 +166,7 @@ typedef enum SDL_ProcessIO
  * - `SDL_PROP_PROCESS_CREATE_ENVIRONMENT_POINTER`: an SDL_Environment
  *   pointer. If this property is set, it will be the entire environment for
  *   the process, otherwise the current environment is used.
+ * - `SDL_PROP_PROCESS_CREATE_WORKING_DIRECTORY_STRING`: a UTF-8 encoded string representing the working directory for the process, defaults to the current working directory.
  * - `SDL_PROP_PROCESS_CREATE_STDIN_NUMBER`: an SDL_ProcessIO value describing
  *   where standard input for the process comes from, defaults to
  *   `SDL_PROCESS_STDIO_NULL`.
@@ -219,6 +220,7 @@ extern SDL_DECLSPEC SDL_Process * SDLCALL SDL_CreateProcessWithProperties(SDL_Pr
 
 #define SDL_PROP_PROCESS_CREATE_ARGS_POINTER                "SDL.process.create.args"
 #define SDL_PROP_PROCESS_CREATE_ENVIRONMENT_POINTER         "SDL.process.create.environment"
+#define SDL_PROP_PROCESS_CREATE_WORKING_DIRECTORY_STRING    "SDL.process.create.working_directory"
 #define SDL_PROP_PROCESS_CREATE_STDIN_NUMBER                "SDL.process.create.stdin_option"
 #define SDL_PROP_PROCESS_CREATE_STDIN_POINTER               "SDL.process.create.stdin_source"
 #define SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER               "SDL.process.create.stdout_option"
diff --git a/include/build_config/SDL_build_config.h.cmake b/include/build_config/SDL_build_config.h.cmake
index 4b40e281c535f..c8c02894a13fb 100644
--- a/include/build_config/SDL_build_config.h.cmake
+++ b/include/build_config/SDL_build_config.h.cmake
@@ -228,6 +228,8 @@
 #cmakedefine HAVE_SHOBJIDL_CORE_H 1
 
 #cmakedefine USE_POSIX_SPAWN 1
+#cmakedefine HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR 1
+#cmakedefine HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP 1
 
 /* SDL internal assertion support */
 #cmakedefine SDL_DEFAULT_ASSERT_LEVEL_CONFIGURED 1
diff --git a/src/process/posix/SDL_posixprocess.c b/src/process/posix/SDL_posixprocess.c
index 98b9f75bca8e8..2e05b01324ecc 100644
--- a/src/process/posix/SDL_posixprocess.c
+++ b/src/process/posix/SDL_posixprocess.c
@@ -37,6 +37,12 @@
 #include "../../io/SDL_iostream_c.h"
 
 
+#if defined(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP) && \
+    !defined(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR)
+#define HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR
+#define posix_spawn_file_actions_addchdir posix_spawn_file_actions_addchdir_np
+#endif
+
 #define READ_END 0
 #define WRITE_END 1
 
@@ -156,6 +162,7 @@ bool SDL_SYS_CreateProcessWithProperties(SDL_Process *process, SDL_PropertiesID
     char * const *args = SDL_GetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ARGS_POINTER, NULL);
     SDL_Environment *env = SDL_GetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ENVIRONMENT_POINTER, SDL_GetEnvironment());
     char **envp = NULL;
+    const char *working_directory = SDL_GetStringProperty(props, SDL_PROP_PROCESS_CREATE_WORKING_DIRECTORY_STRING, NULL);
     SDL_ProcessIO stdin_option = (SDL_ProcessIO)SDL_GetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDIN_NUMBER, SDL_PROCESS_STDIO_NULL);
     SDL_ProcessIO stdout_option = (SDL_ProcessIO)SDL_GetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROCESS_STDIO_INHERITED);
     SDL_ProcessIO stderr_option = (SDL_ProcessIO)SDL_GetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_NUMBER, SDL_PROCESS_STDIO_INHERITED);
@@ -192,6 +199,30 @@ bool SDL_SYS_CreateProcessWithProperties(SDL_Process *process, SDL_PropertiesID
         goto posix_spawn_fail_attr;
     }
 
+    if (working_directory) {
+#ifdef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR
+#ifdef SDL_PLATFORM_APPLE
+        if (__builtin_available(macOS 10.15, *)) {
+            if (posix_spawn_file_actions_addchdir(&fa, working_directory) != 0) {
+                SDL_SetError("posix_spawn_file_actions_addchdir failed: %s", strerror(errno));
+                goto posix_spawn_fail_all;
+            }
+        } else {
+            SDL_SetError("Setting the working directory is only supported on macOS 10.15 and newer");
+            goto posix_spawn_fail_all;
+        }
+#else
+        if (posix_spawn_file_actions_addchdir(&fa, working_directory) != 0) {
+            SDL_SetError("posix_spawn_file_actions_addchdir failed: %s", strerror(errno));
+            goto posix_spawn_fail_all;
+        }
+#endif // SDL_PLATFORM_APPLE
+#else
+        SDL_SetError("Setting the working directory is not supported");
+        goto posix_spawn_fail_all;
+#endif
+    }
+
     // Background processes don't have access to the terminal
     if (process->background) {
         if (stdin_option == SDL_PROCESS_STDIO_INHERITED) {
diff --git a/src/process/windows/SDL_windowsprocess.c b/src/process/windows/SDL_windowsprocess.c
index c1aee5c4ab2cb..95f45c08f14b6 100644
--- a/src/process/windows/SDL_windowsprocess.c
+++ b/src/process/windows/SDL_windowsprocess.c
@@ -239,6 +239,7 @@ bool SDL_SYS_CreateProcessWithProperties(SDL_Process *process, SDL_PropertiesID
     const char * const *args = SDL_GetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ARGS_POINTER, NULL);
     SDL_Environment *env = SDL_GetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ENVIRONMENT_POINTER, SDL_GetEnvironment());
     char **envp = NULL;
+    const char *working_directory = SDL_GetStringProperty(props, SDL_PROP_PROCESS_CREATE_WORKING_DIRECTORY_STRING, NULL);
     SDL_ProcessIO stdin_option = (SDL_ProcessIO)SDL_GetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDIN_NUMBER, SDL_PROCESS_STDIO_NULL);
     SDL_ProcessIO stdout_option = (SDL_ProcessIO)SDL_GetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROCESS_STDIO_INHERITED);
     SDL_ProcessIO stderr_option = (SDL_ProcessIO)SDL_GetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_NUMBER, SDL_PROCESS_STDIO_INHERITED);
@@ -246,6 +247,7 @@ bool SDL_SYS_CreateProcessWithProperties(SDL_Process *process, SDL_PropertiesID
                            !SDL_HasProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_NUMBER);
     LPWSTR createprocess_cmdline = NULL;
     LPWSTR createprocess_env = NULL;
+    LPWSTR createprocess_cwd = NULL;
     STARTUPINFOW startup_info;
     DWORD creation_flags;
     SECURITY_ATTRIBUTES security_attributes;
@@ -292,6 +294,13 @@ bool SDL_SYS_CreateProcessWithProperties(SDL_Process *process, SDL_PropertiesID
         goto done;
     }
 
+    if (working_directory) {
+        createprocess_cwd = WIN_UTF8ToStringW(working_directory);
+        if (!createprocess_cwd) {
+            goto done;
+        }
+    }
+
     // Background processes don't have access to the terminal
     // This isn't necessary on Windows, but we keep the same behavior as the POSIX implementation.
     if (process->background) {
@@ -427,7 +436,7 @@ bool SDL_SYS_CreateProcessWithProperties(SDL_Process *process, SDL_PropertiesID
         }
     }
 
-    if (!CreateProcessW(NULL, createprocess_cmdline, NULL, NULL, TRUE, creation_flags, createprocess_env, NULL, &startup_info, &data->process_information)) {
+    if (!CreateProcessW(NULL, createprocess_cmdline, NULL, NULL, TRUE, creation_flags, createprocess_env, createprocess_cwd, &startup_info, &data->process_information)) {
         WIN_SetError("CreateProcess");
         goto done;
     }
@@ -479,6 +488,7 @@ bool SDL_SYS_CreateProcessWithProperties(SDL_Process *process, SDL_PropertiesID
     }
     SDL_free(createprocess_cmdline);
     SDL_free(createprocess_env);
+    SDL_free(createprocess_cwd);
     SDL_free(envp);
 
     if (!result) {