diff --git a/docs/steam-compat-tool-interface.md b/docs/steam-compat-tool-interface.md index ebeac3c6189b8faf64983efafc018ffee65acfb1..7fb910d69725725b9ca15fb6efe8a1e199dfad23 100644 --- a/docs/steam-compat-tool-interface.md +++ b/docs/steam-compat-tool-interface.md @@ -298,6 +298,24 @@ Some environment variables are set by Steam, including: game's metadata, affecting the functioning of various compatibility tools: + * `runtime-sdl2`: Use the [`$SDL_DYNAMIC_API` mechanism][SDL_DYNAMIC_API] + to load the version of SDL 2 included in + Steam Linux Runtime 2 (soldier) or later, + in preference to a bundled or statically linked SDL 2 that might be + included in the game itself. + For example, this can be used to ensure that older Source-engine + titles like [Fistful of Frags][] + runs with a version of SDL that supports game controller hotplug + inside a container, even though they include a bundled version + that does not. + Setting `STEAM_COMPAT_RUNTIME_SDL2=1` is equivalent to this, + and is more convenient to use in Launch Options, by setting the + Launch Options to `STEAM_COMPAT_RUNTIME_SDL2=1 %command%`. + + * `runtime-sdl3`: Same as `runtime-sdl2`, but for SDL 3 (if available), + using [`$SDL3_DYNAMIC_API`][SDL3_DYNAMIC_API]. + Setting `STEAM_COMPAT_RUNTIME_SDL3=1` is equivalent to this. + * `search-cwd`: Instructs the legacy `LD_LIBRARY_PATH` runtime to append the `${STEAM_COMPAT_INSTALL_PATH}` to the `LD_LIBRARY_PATH`, for backward compatibility with older games that might rely on this @@ -368,6 +386,20 @@ Some environment variables are set by Steam, including: Colon-delimited list of paths to additional directories that are to be made available read/write in a pressure-vessel container. +* `STEAM_COMPAT_RUNTIME_SDL2`: + + May be set to 1 for the same effect as adding `runtime-sdl2` to + `$STEAM_COMPAT_FLAGS`. + (For example, a game's Launch Options can be set to + `STEAM_COMPAT_RUNTIME_SDL2=1 %command%`) + +* `STEAM_COMPAT_RUNTIME_SDL3`: + + May be set to 1 for the same effect as adding `runtime-sdl3` to + `$STEAM_COMPAT_FLAGS`. + (For example, a game's Launch Options can be set to + `STEAM_COMPAT_RUNTIME_SDL3=1 %command%`) + * `STEAM_COMPAT_SESSION_ID`: In the historical session mode (see below), this is used to link together @@ -611,6 +643,8 @@ unless this is set to the game's top-level directory. <!-- References: --> +[SDL_DYNAMIC_API]: https://github.com/libsdl-org/SDL/blob/SDL2/docs/README-dynapi.md +[SDL3_DYNAMIC_API]: https://github.com/libsdl-org/SDL/blob/main/docs/README-dynapi.md [Steamworks launch options]: https://partner.steamgames.com/doc/sdk/uploading [VDF]: https://developer.valvesoftware.com/wiki/KeyValues [app891390-info]: https://steamdb.info/app/891390/info/ diff --git a/pressure-vessel/adverb-sdl.c b/pressure-vessel/adverb-sdl.c new file mode 100644 index 0000000000000000000000000000000000000000..72474c4b687f7f801f00a588ce952cc8fd72e38d --- /dev/null +++ b/pressure-vessel/adverb-sdl.c @@ -0,0 +1,148 @@ +/* + * Copyright © 2024 Collabora Ltd. + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "config.h" +#include "adverb-sdl.h" + +gboolean +pv_adverb_set_up_dynamic_sdl (FlatpakBwrap *wrapped_command, + PvPerArchDirs *lib_temp_dirs, + const char *prefix, + const char *overrides, + const char *dynamic_var, + const char *soname, + GError **error) +{ + gboolean have_any = FALSE; + size_t abi_index; + const char *existing_value; + + existing_value = g_environ_getenv (wrapped_command->envp, dynamic_var); + + if (existing_value != NULL) + { + /* Treat SDL{,3}_DYNAMIC_API from e.g. launch options as being + * more important than STEAM_COMPAT_FLAGS=runtime-sdl{2,3} */ + g_message ("Not using %s from runtime because %s is already set to \"%s\"", + soname, dynamic_var, existing_value); + return TRUE; + } + + if (lib_temp_dirs == NULL) + return glnx_throw (error, + "Cannot set up dynamic %s without per-architecture directories", + soname); + + for (abi_index = 0; abi_index < PV_N_SUPPORTED_ARCHITECTURES; abi_index++) + { + const char *multiarch_tuple = pv_multiarch_tuples[abi_index]; + g_autofree gchar *dest = NULL; + g_autofree gchar *from_runtime = NULL; + g_autofree gchar *override = NULL; + const char *target; + + /* We assume a Debian multiarch layout here: in practice this + * is true for all Steam Runtime branches. */ + from_runtime = g_build_filename (prefix, "lib", multiarch_tuple, soname, NULL); + override = g_build_filename (overrides, "lib", multiarch_tuple, soname, NULL); + + if (g_file_test (override, G_FILE_TEST_EXISTS)) + { + /* This is quite unexpected - we hope that none of the + * graphics drivers and Vulkan layers will have pulled in + * something as big as SDL, because if they do, that really + * undermines what we're trying to achieve. But if something + * in the graphics stack does depend on SDL, we really have + * no choice but to use that version. */ + g_message ("Using %s %s from graphics stack provider instead of runtime", + multiarch_tuple, soname); + target = override; + } + else if (g_file_test (from_runtime, G_FILE_TEST_EXISTS)) + { + target = from_runtime; + } + else + { + g_info ("%s doesn't exist in container", from_runtime); + continue; + } + + dest = g_build_filename (lib_temp_dirs->abi_paths[abi_index], + soname, NULL); + g_info ("Creating symlink \"%s\" -> \"%s\" in container", + dest, target); + + if (symlink (target, dest) != 0) + return glnx_throw_errno_prefix (error, + "While creating symlink \"%s\"", + dest); + + have_any = TRUE; + } + + if (have_any) + { + g_autofree gchar *value = NULL; + + value = g_build_filename (lib_temp_dirs->libdl_token_path, soname, NULL); + g_info ("Setting %s=\"%s\" to use runtime's SDL", dynamic_var, soname); + flatpak_bwrap_set_env (wrapped_command, dynamic_var, value, TRUE); + } + else + { + return glnx_throw (error, + "Unable to set %s: %s wasn't available for any architecture", + dynamic_var, soname); + } + + return TRUE; +} + +void +pv_adverb_set_up_dynamic_sdls (FlatpakBwrap *wrapped_command, + PvPerArchDirs *lib_temp_dirs, + const char *prefix, + const char *overrides, + SrtSteamCompatFlags compat_flags) +{ + static const struct + { + const char *dynamic_var; + const char *soname; + SrtSteamCompatFlags if_flag; + } + sdls[] = + { + { + "SDL_DYNAMIC_API", + "libSDL2-2.0.so.0", + SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL2 + }, + { + "SDL3_DYNAMIC_API", + "libSDL3.so.0", + SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL3 + }, + }; + size_t i; + + for (i = 0; i < G_N_ELEMENTS (sdls); i++) + { + if (compat_flags & sdls[i].if_flag) + { + g_autoptr(GError) local_error = NULL; + + if (!pv_adverb_set_up_dynamic_sdl (wrapped_command, + lib_temp_dirs, + prefix, + overrides, + sdls[i].dynamic_var, + sdls[i].soname, + &local_error)) + g_warning ("%s", local_error->message); + } + } +} diff --git a/pressure-vessel/adverb-sdl.h b/pressure-vessel/adverb-sdl.h new file mode 100644 index 0000000000000000000000000000000000000000..a4fe9d4caa835b91c9d9e8dc0cd4aac93ff71df7 --- /dev/null +++ b/pressure-vessel/adverb-sdl.h @@ -0,0 +1,29 @@ +/* + * Copyright © 2024 Collabora Ltd. + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include "libglnx.h" + +#include <glib.h> + +#include "steam-runtime-tools/steam-internal.h" + +#include "flatpak-bwrap-private.h" +#include "per-arch-dirs.h" + +gboolean pv_adverb_set_up_dynamic_sdl (FlatpakBwrap *wrapped_command, + PvPerArchDirs *lib_temp_dirs, + const char *prefix, + const char *overrides, + const char *dynamic_var, + const char *soname, + GError **error); + +void pv_adverb_set_up_dynamic_sdls (FlatpakBwrap *wrapped_command, + PvPerArchDirs *lib_temp_dirs, + const char *prefix, + const char *overrides, + SrtSteamCompatFlags compat_flags); diff --git a/pressure-vessel/adverb.c b/pressure-vessel/adverb.c index d5a33d9341441d6d4cb05528464b23f30460b84a..bd9499d0c741cc6721e04d94e85f685164a4952b 100644 --- a/pressure-vessel/adverb.c +++ b/pressure-vessel/adverb.c @@ -46,6 +46,7 @@ #include "libglnx.h" #include "adverb-preload.h" +#include "adverb-sdl.h" #include "flatpak-utils-base-private.h" #include "per-arch-dirs.h" #include "supported-architectures.h" @@ -849,6 +850,7 @@ main (int argc, glnx_autofd int original_stderr = -1; g_autoptr(FlatpakBwrap) wrapped_command = NULL; g_autoptr(PvPerArchDirs) lib_temp_dirs = NULL; + SrtSteamCompatFlags compat_flags; setlocale (LC_ALL, ""); @@ -1086,6 +1088,10 @@ main (int argc, g_clear_error (error); } + compat_flags = _srt_steam_get_compat_flags (_srt_const_strv (envp)); + pv_adverb_set_up_dynamic_sdls (wrapped_command, lib_temp_dirs, + "/usr", opt_overrides, compat_flags); + if (opt_overrides != NULL && !pv_adverb_set_up_overrides (wrapped_command, lib_temp_dirs, diff --git a/pressure-vessel/meson.build b/pressure-vessel/meson.build index 1394f188d2b38459df29ca3071808666878db631..245e313c7941edfda395a04a39c0e40fdb6dc808 100644 --- a/pressure-vessel/meson.build +++ b/pressure-vessel/meson.build @@ -108,6 +108,8 @@ pressure_vessel_adverb_lib = static_library( sources : [ 'adverb-preload.c', 'adverb-preload.h', + 'adverb-sdl.c', + 'adverb-sdl.h', 'per-arch-dirs.c', 'per-arch-dirs.h', 'wrap-interactive.c', diff --git a/steam-runtime-tools/steam-internal.h b/steam-runtime-tools/steam-internal.h index c0a5f9f4957f644f0db98a72910c3c97021e03cc..9cdf16149d4001ec065faa9d05e41ef13a9c1373 100644 --- a/steam-runtime-tools/steam-internal.h +++ b/steam-runtime-tools/steam-internal.h @@ -78,6 +78,8 @@ typedef enum SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD = (1 << 0), SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD_FIRST = (1 << 1), SRT_STEAM_COMPAT_FLAGS_SYSTEM_TRACING = (1 << 2), + SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL2 = (1 << 3), + SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL3 = (1 << 4), SRT_STEAM_COMPAT_FLAGS_NONE = 0 } SrtSteamCompatFlags; diff --git a/steam-runtime-tools/steam.c b/steam-runtime-tools/steam.c index 86e0997469e462fd14205bff3f9643546a8debc6..ff9bd7242922124dadb844d3679410c84db266ba 100644 --- a/steam-runtime-tools/steam.c +++ b/steam-runtime-tools/steam.c @@ -751,21 +751,37 @@ _srt_steam_check (const char * const *envp, SrtSteamCompatFlags _srt_steam_get_compat_flags (const char * const *envp) { + static const struct + { + const char *name; + SrtSteamCompatFlags value; + gboolean def; + } + bool_vars[] = + { + { "STEAM_COMPAT_TRACING", SRT_STEAM_COMPAT_FLAGS_SYSTEM_TRACING, FALSE }, + { "STEAM_COMPAT_RUNTIME_SDL2", SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL2, FALSE }, + { "STEAM_COMPAT_RUNTIME_SDL3", SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL3, FALSE }, + }; SrtSteamCompatFlags ret = SRT_STEAM_COMPAT_FLAGS_NONE; const char *value; - gboolean tracing = FALSE; + size_t i; - _srt_environ_get_boolean (envp, "STEAM_COMPAT_TRACING", &tracing, NULL); + for (i = 0; i < G_N_ELEMENTS (bool_vars); i++) + { + gboolean bool_value = bool_vars[i].def; - if (tracing) - ret |= SRT_STEAM_COMPAT_FLAGS_SYSTEM_TRACING; + _srt_environ_get_boolean (envp, bool_vars[i].name, &bool_value, NULL); + + if (bool_value) + ret |= bool_vars[i].value; + } value = _srt_environ_getenv (envp, "STEAM_COMPAT_FLAGS"); if (value != NULL) { g_auto(GStrv) tokens = NULL; - size_t i; tokens = g_strsplit (value, ",", 0); @@ -773,6 +789,14 @@ _srt_steam_get_compat_flags (const char * const *envp) { switch (tokens[i][0]) { + case 'r': + if (g_str_equal (tokens[i], "runtime-sdl2")) + ret |= SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL2; + else if (g_str_equal (tokens[i], "runtime-sdl3")) + ret |= SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL3; + + break; + case 's': if (g_str_equal (tokens[i], "search-cwd")) ret |= SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD; diff --git a/tests/pressure-vessel/adverb-sdl.c b/tests/pressure-vessel/adverb-sdl.c new file mode 100644 index 0000000000000000000000000000000000000000..d6fde6ef8382bbc0591199b871ee950757854901 --- /dev/null +++ b/tests/pressure-vessel/adverb-sdl.c @@ -0,0 +1,451 @@ +/* + * Copyright © 2023-2024 Collabora Ltd. + * SPDX-License-Identifier: MIT + */ + +#include <glib.h> + +#include "steam-runtime-tools/glib-backports-internal.h" +#include "steam-runtime-tools/utils-internal.h" +#include "libglnx.h" + +#include "adverb-sdl.h" +#include "tests/test-utils.h" + +#define SDL_DYNAMIC_API "SDL_DYNAMIC_API" +#define SDL2_SONAME "libSDL2-2.0.so.0" +#define SDL3_DYNAMIC_API "SDL3_DYNAMIC_API" +#define SDL3_SONAME "libSDL3.so.0" + +typedef struct +{ + TestsOpenFdSet old_fds; + FlatpakBwrap *bwrap; + PvPerArchDirs *lib_temp_dirs; + GError *lib_temp_dirs_error; + GLnxTmpDir mock_prefix; + GLnxTmpDir mock_overrides; +} Fixture; + +typedef struct +{ + int unused; +} Config; + +static void +setup (Fixture *f, + gconstpointer context) +{ + G_GNUC_UNUSED const Config *config = context; + g_autoptr(GError) local_error = NULL; + gsize i; + + f->old_fds = tests_check_fd_leaks_enter (); + f->bwrap = flatpak_bwrap_new (flatpak_bwrap_empty_env); + f->lib_temp_dirs = pv_per_arch_dirs_new (&f->lib_temp_dirs_error); + + if (f->lib_temp_dirs != NULL) + { + g_test_message ("Cross-platform module prefix: %s", + f->lib_temp_dirs->libdl_token_path); + + for (i = 0; i < PV_N_SUPPORTED_ARCHITECTURES; i++) + g_test_message ("Concrete path for %s architecture: %s", + pv_multiarch_tuples[i], + f->lib_temp_dirs->abi_paths[i]); + } + + glnx_mkdtemp ("usr-XXXXXX", 0700, &f->mock_prefix, &local_error); + g_assert_no_error (local_error); + + glnx_mkdtemp ("overrides-XXXXXX", 0700, &f->mock_overrides, &local_error); + g_assert_no_error (local_error); +} + +static void +teardown (Fixture *f, + gconstpointer context) +{ + G_GNUC_UNUSED const Config *config = context; + + g_clear_pointer (&f->lib_temp_dirs, pv_per_arch_dirs_free); + g_clear_error (&f->lib_temp_dirs_error); + g_clear_pointer (&f->bwrap, flatpak_bwrap_free); + glnx_tmpdir_cleanup (&f->mock_prefix); + glnx_tmpdir_cleanup (&f->mock_overrides); + tests_check_fd_leaks_leave (f->old_fds); +} + +static void touch (gchar **, const char *, ...) G_GNUC_NULL_TERMINATED; +static void +touch (gchar **path_out, + const char *path, + ...) +{ + g_autoptr(GError) local_error = NULL; + g_autofree gchar *joined = NULL; + g_autofree gchar *parent = NULL; + va_list ap; + + va_start (ap, path); + joined = g_build_filename_valist (path, &ap); + va_end (ap); + parent = g_path_get_dirname (joined); + + glnx_shutil_mkdir_p_at (AT_FDCWD, parent, 0755, NULL, &local_error); + g_assert_no_error (local_error); + glnx_file_replace_contents_at (AT_FDCWD, joined, (const guint8 *) "", 0, + (GLNX_FILE_REPLACE_NODATASYNC + | GLNX_FILE_REPLACE_INCREASING_MTIME), + NULL, &local_error); + g_assert_no_error (local_error); + + if (path_out != NULL) + *path_out = g_steal_pointer (&joined); +} + +static void +assert_symlink (const char *arch_dir, + const char *soname, + const char *expect_target, + GIOErrorEnum expect_error) +{ + g_autoptr(GError) local_error = NULL; + g_autofree gchar *joined = NULL; + g_autofree gchar *target = NULL; + + joined = g_build_filename (arch_dir, soname, NULL); + target = glnx_readlinkat_malloc (AT_FDCWD, joined, NULL, &local_error); + + if (target != NULL) + g_test_message ("%s -> %s", joined, target); + else + g_test_message ("%s doesn't exist", joined); + + if (expect_target != NULL) + { + g_assert_no_error (local_error); + g_assert_cmpstr (target, ==, expect_target); + } + else + { + g_assert_error (local_error, G_IO_ERROR, expect_error); + g_assert_null (target); + } +} + +static void +assert_env (gchar **envp, + const char *var, + const char *expected_dir, + const char *expected_file) +{ + g_autofree gchar *expected = NULL; + const char *actual; + + if (expected_dir != NULL) + expected = g_build_filename (expected_dir, expected_file, NULL); + + actual = g_environ_getenv (envp, var); + + if (actual != NULL) + g_test_message ("%s=%s", var, actual); + else + g_test_message ("$%s is unset", var); + + g_assert_cmpstr (actual, ==, expected); +} + +static void +test_basic (Fixture *f, + gconstpointer context) +{ + g_autofree gchar *sdl2_target = NULL; + g_autofree gchar *sdl3_target = NULL; + + if (f->lib_temp_dirs == NULL) + { + g_test_skip (f->lib_temp_dirs_error->message); + return; + } + + g_test_summary ("Basic setup of " SDL_DYNAMIC_API); + + touch (&sdl2_target, + f->mock_prefix.path, "lib", pv_multiarch_tuples[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL); + touch (&sdl3_target, + f->mock_prefix.path, "lib", pv_multiarch_tuples[PV_PRIMARY_ARCHITECTURE], + SDL3_SONAME, NULL); + + g_test_message ("With no flags, not setup is done..."); + pv_adverb_set_up_dynamic_sdls (f->bwrap, + f->lib_temp_dirs, + f->mock_prefix.path, + f->mock_overrides.path, + SRT_STEAM_COMPAT_FLAGS_NONE); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL, G_IO_ERROR_NOT_FOUND); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL3_SONAME, NULL, G_IO_ERROR_NOT_FOUND); + assert_env (f->bwrap->envp, SDL_DYNAMIC_API, NULL, NULL); + assert_env (f->bwrap->envp, SDL3_DYNAMIC_API, NULL, NULL); + + g_test_message ("SDL2 flag sets up SDL2..."); + pv_adverb_set_up_dynamic_sdls (f->bwrap, + f->lib_temp_dirs, + f->mock_prefix.path, + f->mock_overrides.path, + SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL2); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, sdl2_target, 0); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL3_SONAME, NULL, G_IO_ERROR_NOT_FOUND); + assert_env (f->bwrap->envp, SDL_DYNAMIC_API, + f->lib_temp_dirs->libdl_token_path, SDL2_SONAME); + assert_env (f->bwrap->envp, SDL3_DYNAMIC_API, NULL, NULL); + + g_test_message ("SDL3 flag additionally sets up SDL3..."); + pv_adverb_set_up_dynamic_sdls (f->bwrap, + f->lib_temp_dirs, + f->mock_prefix.path, + f->mock_overrides.path, + SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL3); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, sdl2_target, 0); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL3_SONAME, sdl3_target, 0); + assert_env (f->bwrap->envp, SDL_DYNAMIC_API, + f->lib_temp_dirs->libdl_token_path, SDL2_SONAME); + assert_env (f->bwrap->envp, SDL3_DYNAMIC_API, + f->lib_temp_dirs->libdl_token_path, SDL3_SONAME); +} + +static void +test_cannot_symlink (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) local_error = NULL; + g_autofree gchar *sdl2_target = NULL; + gboolean res; + + if (f->lib_temp_dirs == NULL) + { + g_test_skip (f->lib_temp_dirs_error->message); + return; + } + + touch (&sdl2_target, + f->mock_prefix.path, "lib", pv_multiarch_tuples[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL); + touch (NULL, + f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL); + + g_test_summary ("If we can't create the symlink, setup fails"); + res = pv_adverb_set_up_dynamic_sdl (f->bwrap, + f->lib_temp_dirs, + f->mock_prefix.path, + f->mock_overrides.path, + SDL_DYNAMIC_API, + SDL2_SONAME, + &local_error); + g_assert_nonnull (local_error); + /* pv-adverb would log the GError as a warning */ + g_test_message ("%s", local_error->message); + g_assert_false (res); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL, G_IO_ERROR_INVALID_ARGUMENT); + assert_env (f->bwrap->envp, SDL_DYNAMIC_API, NULL, NULL); +} + +static void +test_impossible (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) local_error = NULL; + g_autofree gchar *sdl2_target = NULL; + gboolean res; + + touch (&sdl2_target, + f->mock_prefix.path, "lib", pv_multiarch_tuples[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL); + + g_test_summary ("If we don't know the $LIB or $PLATFORM, nothing happens, " + "with a warning"); + res = pv_adverb_set_up_dynamic_sdl (f->bwrap, + NULL, + f->mock_prefix.path, + f->mock_overrides.path, + SDL_DYNAMIC_API, + SDL2_SONAME, + &local_error); + g_assert_nonnull (local_error); + /* pv-adverb would log the GError as a warning */ + g_test_message ("%s", local_error->message); + g_assert_false (res); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL, G_IO_ERROR_NOT_FOUND); + assert_env (f->bwrap->envp, SDL_DYNAMIC_API, NULL, NULL); +} + +static void +test_in_gfx_stack (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) local_error = NULL; + g_autofree gchar *sdl2_target = NULL; + g_autofree gchar *gfx_sdl2_target = NULL; + gboolean res; + + if (f->lib_temp_dirs == NULL) + { + g_test_skip (f->lib_temp_dirs_error->message); + return; + } + + touch (&sdl2_target, + f->mock_prefix.path, "lib", pv_multiarch_tuples[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL); + touch (&gfx_sdl2_target, + f->mock_overrides.path, "lib", pv_multiarch_tuples[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL); + + g_test_summary ("We prefer SDL from the graphics provider if present"); + res = pv_adverb_set_up_dynamic_sdl (f->bwrap, + f->lib_temp_dirs, + f->mock_prefix.path, + f->mock_overrides.path, + SDL_DYNAMIC_API, + SDL2_SONAME, + &local_error); + g_assert_no_error (local_error); + g_assert_true (res); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, gfx_sdl2_target, 0); + assert_env (f->bwrap->envp, SDL_DYNAMIC_API, + f->lib_temp_dirs->libdl_token_path, SDL2_SONAME); +} + +static void +test_in_gfx_stack_only (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) local_error = NULL; + g_autofree gchar *gfx_sdl2_target = NULL; + gboolean res; + + if (f->lib_temp_dirs == NULL) + { + g_test_skip (f->lib_temp_dirs_error->message); + return; + } + + touch (&gfx_sdl2_target, + f->mock_overrides.path, "lib", pv_multiarch_tuples[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL); + + g_test_summary ("We use SDL from the graphics provider if necessary"); + res = pv_adverb_set_up_dynamic_sdl (f->bwrap, + f->lib_temp_dirs, + f->mock_prefix.path, + f->mock_overrides.path, + SDL_DYNAMIC_API, + SDL2_SONAME, + &local_error); + g_assert_no_error (local_error); + g_assert_true (res); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, gfx_sdl2_target, 0); + assert_env (f->bwrap->envp, SDL_DYNAMIC_API, + f->lib_temp_dirs->libdl_token_path, SDL2_SONAME); +} + +static void +test_missing (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) local_error = NULL; + gboolean res; + + if (f->lib_temp_dirs == NULL) + { + g_test_skip (f->lib_temp_dirs_error->message); + return; + } + + g_test_summary ("If SDL is missing, nothing happens, with a warning"); + res = pv_adverb_set_up_dynamic_sdl (f->bwrap, + f->lib_temp_dirs, + f->mock_prefix.path, + f->mock_overrides.path, + SDL_DYNAMIC_API, + SDL2_SONAME, + &local_error); + g_assert_nonnull (local_error); + /* pv-adverb would log the GError as a warning */ + g_test_message ("%s", local_error->message); + g_assert_false (res); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL, G_IO_ERROR_NOT_FOUND); + assert_env (f->bwrap->envp, SDL_DYNAMIC_API, NULL, NULL); +} + +static void +test_overridden (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) local_error = NULL; + gboolean res; + + if (f->lib_temp_dirs == NULL) + { + g_test_skip (f->lib_temp_dirs_error->message); + return; + } + + g_test_summary ("Setting " SDL_DYNAMIC_API " takes precedence"); + flatpak_bwrap_set_env (f->bwrap, SDL_DYNAMIC_API, "/whatever", TRUE); + res = pv_adverb_set_up_dynamic_sdl (f->bwrap, + f->lib_temp_dirs, + f->mock_prefix.path, + f->mock_overrides.path, + SDL_DYNAMIC_API, + SDL2_SONAME, + &local_error); + g_assert_no_error (local_error); + g_assert_true (res); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL, G_IO_ERROR_NOT_FOUND); + assert_env (f->bwrap->envp, SDL_DYNAMIC_API, "/whatever", NULL); +} + +int +main (int argc, + char **argv) +{ + _srt_setenv_disable_gio_modules (); + + /* In unit tests it isn't always straightforward to find the real + * ${PLATFORM}, so use a predictable mock implementation that always + * uses PvMultiarchDetails.platforms[0] */ + g_setenv ("PRESSURE_VESSEL_TEST_STANDARDIZE_PLATFORM", "1", TRUE); + + _srt_tests_init (&argc, &argv, NULL); + g_test_add ("/pv-adverb/sdl/basic", Fixture, NULL, + setup, test_basic, teardown); + g_test_add ("/pv-adverb/sdl/cannot-symlink", Fixture, NULL, + setup, test_cannot_symlink, teardown); + g_test_add ("/pv-adverb/sdl/impossible", Fixture, NULL, + setup, test_impossible, teardown); + g_test_add ("/pv-adverb/sdl/in_gfx_stack", Fixture, NULL, + setup, test_in_gfx_stack, teardown); + g_test_add ("/pv-adverb/sdl/in_gfx_stack_only", Fixture, NULL, + setup, test_in_gfx_stack_only, teardown); + g_test_add ("/pv-adverb/sdl/missing", Fixture, NULL, + setup, test_missing, teardown); + g_test_add ("/pv-adverb/sdl/overridden", Fixture, NULL, + setup, test_overridden, teardown); + + return g_test_run (); +} diff --git a/tests/pressure-vessel/meson.build b/tests/pressure-vessel/meson.build index c36e79474516fb78647a7b46bc9c3003a269010a..c7cd13675e7ac4f7d78b88c9a6b6e706b6bc2eca 100644 --- a/tests/pressure-vessel/meson.build +++ b/tests/pressure-vessel/meson.build @@ -68,6 +68,7 @@ tests = [ compiled_tests = [ {'name': 'adverb-preload', 'adverb': true}, + {'name': 'adverb-sdl', 'adverb': true}, {'name': 'bwrap', 'wrap': true}, {'name': 'graphics-provider', 'wrap': true}, {'name': 'wrap-setup', 'wrap': true}, diff --git a/tests/utils.c b/tests/utils.c index c0128cd8cdb471d036a647dee79520144a6cae9d..a70beed3320615a71701e62b465e9d77c01ed5f4 100644 --- a/tests/utils.c +++ b/tests/utils.c @@ -129,18 +129,53 @@ test_compat_flags (Fixture *f, { static const struct { - const char *env; + /* Length is arbitrary, expand as needed but leave room for NULL + * termination */ + const char * const envp[3]; SrtSteamCompatFlags expected; } tests[] = { - { "search-cwd,search-cwd-first,reticulate-splines,fixme", - (SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD - | SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD_FIRST) }, - { "reticulate-splines,search-cwd", SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD }, - { ",,,,search-cwd-first,,,,", SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD_FIRST }, - { "", SRT_STEAM_COMPAT_FLAGS_NONE }, - { NULL, SRT_STEAM_COMPAT_FLAGS_NONE } + { + { + "STEAM_COMPAT_FLAGS=search-cwd,search-cwd-first,reticulate-splines,fixme", + NULL + }, + (SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD + | SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD_FIRST), + }, + { + { "STEAM_COMPAT_FLAGS=reticulate-splines,search-cwd", NULL }, + SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD, + }, + { + { "STEAM_COMPAT_FLAGS=,,,,search-cwd-first,,,,", NULL }, + SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD_FIRST, + }, + { + { "STEAM_COMPAT_FLAGS=runtime-sdl2", NULL }, + SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL2, + }, + { + { "STEAM_COMPAT_FLAGS=runtime-sdl3", NULL }, + SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL3, + }, + { + { + "STEAM_COMPAT_TRACING=1", + "STEAM_COMPAT_FLAGS=search-cwd", + NULL + }, + (SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD + | SRT_STEAM_COMPAT_FLAGS_SYSTEM_TRACING), + }, + { { "STEAM_COMPAT_FLAGS=", NULL }, SRT_STEAM_COMPAT_FLAGS_NONE }, + { { "STEAM_COMPAT_TRACING=1", NULL }, SRT_STEAM_COMPAT_FLAGS_SYSTEM_TRACING }, + { { "STEAM_COMPAT_TRACING=", NULL }, SRT_STEAM_COMPAT_FLAGS_NONE }, + { { "STEAM_COMPAT_TRACING=0", NULL }, SRT_STEAM_COMPAT_FLAGS_NONE }, + { { "STEAM_COMPAT_RUNTIME_SDL2=1", NULL }, SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL2 }, + { { "STEAM_COMPAT_RUNTIME_SDL3=1", NULL }, SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL3 }, + { { NULL }, SRT_STEAM_COMPAT_FLAGS_NONE } }; size_t i; @@ -149,14 +184,10 @@ test_compat_flags (Fixture *f, for (i = 0; i < G_N_ELEMENTS (tests); i++) { - gchar *envp[2] = { NULL, NULL }; - - if (tests[i].env != NULL) - envp[0] = g_strdup_printf ("STEAM_COMPAT_FLAGS=%s", tests[i].env); - - g_assert_cmphex (_srt_steam_get_compat_flags (_srt_const_strv (envp)), + g_assert_cmpuint (g_strv_length ((gchar **) tests[i].envp), + <, G_N_ELEMENTS (tests[i].envp)); + g_assert_cmphex (_srt_steam_get_compat_flags (tests[i].envp), ==, tests[i].expected); - g_free (envp[0]); } }