diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..6e99392 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1785627969, + "narHash": "sha256-4dtXQk/NMePegK/nWp5NSeuZKLATItOq61lpEvmXqGw=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "427bf4bd9435fdf21321c8cc628c24efc14c0f7a", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1787135253, + "narHash": "sha256-RD2kNWCG+Bjo6h+JVjWVNntZs2GtRoeY2xHjts/FNkA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ffb3c9b700e759be2ef13237c9d8f953b32a1e46", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1785031560, + "narHash": "sha256-OmshNvn2vupOFpYinLUu+1Dnpu4n7Q5N3ggGVNHpkUI=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "0e79af5e3d4dcfcd676ab5ba3f95d2e3352e078c", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..a54f6f4 --- /dev/null +++ b/flake.nix @@ -0,0 +1,31 @@ +{ + description = "Description for the project"; + + inputs = { + flake-parts.url = "github:hercules-ci/flake-parts"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = + inputs@{ flake-parts, ... }: + flake-parts.lib.mkFlake { inherit inputs; } { + imports = [ + + ]; + systems = [ + "x86_64-linux" + # "aarch64-linux" + # "aarch64-darwin" + # "x86_64-darwin" + ]; + perSystem = + { + pkgs, + ... + }: + { + packages.default = pkgs.callPackage ./packaging/nix/package.nix { }; + }; + flake = { }; + }; +} diff --git a/packaging/nix/package.nix b/packaging/nix/package.nix new file mode 100644 index 0000000..ebe6fbf --- /dev/null +++ b/packaging/nix/package.nix @@ -0,0 +1,136 @@ +{ + lib, + stdenv, + fetchFromGitHub, + + cmake, + git, + lld, + ninja, + pkg-config, + vulkan-headers, + makeWrapper, + + capstone, + curl, + fontconfig, + glib, + gtk4, + hicolor-icon-theme, + libadwaita, + elfutils, + libglvnd, + libplacebo, + libsoup_3, + libutf8proc, + libyaml, + minizip, + nlohmann_json, + openssl, + sdl3, + sdl3-ttf, + vulkan-loader, + webkitgtk_6_0, + zlib, +}: +let + # submodules are not part of the flake source tree + # so these two vendored dependencies are fetched from their pinned upstream revisions + libjnivmSrc = fetchFromGitHub { + owner = "ChristopherHX"; + repo = "libjnivm"; + rev = "f24b98c198fcc5c59a68d1efadd3f5791eb01c2e"; + hash = "sha256-K4+1Zo7AJIouIqnHn2ZE4GyrNrYOczmWYblLAnGYA5w="; + }; + vulkanHeadersSrc = fetchFromGitHub { + owner = "KhronosGroup"; + repo = "Vulkan-Headers"; + rev = "8d6039a455a7ecc7d2a592ff97f62db4e59b70bf"; + hash = "sha256-+llJlEdzFcmqGBAdKHjYhkwBuH19ZngavmF1TQgejsI="; + }; + + src = ../..; + + lines = lib.splitString "\n" (builtins.readFile (lib.path.append src "CMakeLists.txt")); + + matches = map (builtins.match "[[:space:]]*VERSION[[:space:]]+([0-9][0-9.]*).*") lines; + + match = lib.findFirst (m: m != null) null matches; + + version = + if match == null then throw "could not find VERSION in CMakeLists.txt" else builtins.elemAt match 0; +in +stdenv.mkDerivation (finalAttrs: { + pname = "mocktail"; + inherit version; + inherit src; + + nativeBuildInputs = [ + cmake + git + lld + ninja + pkg-config + vulkan-headers + makeWrapper + ]; + + buildInputs = [ + capstone + curl + fontconfig + glib + gtk4 + hicolor-icon-theme + libadwaita + elfutils + libglvnd + libplacebo + libsoup_3 + libutf8proc + libyaml + minizip + nlohmann_json + openssl + sdl3 + sdl3-ttf + vulkan-loader + webkitgtk_6_0 + zlib + ]; + + preConfigure = '' + rm -rf third_party/libjnivm third_party/Vulkan-Headers + cp -r --no-preserve=mode ${libjnivmSrc} third_party/libjnivm + cp -r --no-preserve=mode ${vulkanHeadersSrc} third_party/Vulkan-Headers + ''; + + # nixpkgs installs the header we need under include/libutf8proc + NIX_CFLAGS_COMPILE = "-I${libutf8proc}/include/libutf8proc"; + + cmakeFlags = [ + "-DCMAKE_BUILD_TYPE=Release" + "-DCMAKE_INSTALL_LIBDIR=lib" + "-DMOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST=${placeholder "out"}/share/mocktail/metadata/roblox_compatibility.json" + "-DMOCKTAIL_DEFAULT_SIGNING_TRUST_MANIFEST=${placeholder "out"}/share/mocktail/metadata/roblox_signing_certificates.json" + "-DBUILD_TESTING=OFF" + ]; + + postInstall = '' + for binary in $out/bin/*; do + wrapProgram "$binary" --prefix LD_LIBRARY_PATH : ${lib.makeLibraryPath finalAttrs.buildInputs} + done + ''; + + fixupPhase = '' + install -Dm644 ${finalAttrs.src}/LICENSE "$out/share/licenses/${finalAttrs.pname}/LICENSE" + ''; + + meta = with lib; { + description = "Android x86-64 Roblox compatibility runtime for Linux"; + homepage = "https://github.com/komaruworld/mocktail"; + license = licenses.asl20; + platforms = platforms.unix; + mainProgram = "mocktail"; + }; +}) diff --git a/src/legacy/legacy_runtime.cc b/src/legacy/legacy_runtime.cc index 4c950f8..0ed6fcc 100644 --- a/src/legacy/legacy_runtime.cc +++ b/src/legacy/legacy_runtime.cc @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -2651,6 +2652,29 @@ struct SymbolResolveResult { SymbolResolveSource source = SymbolResolveSource::kMissing; }; +// dlsym searches the object and its dependency chain, so a +// stub that transitively links SDL or another host library would +// satisfy EGL/GL/Vulkan lookups with unrelated host symbols +// +// So, we should only accept the symbol if its stub owns it. +bool StubOwnsSymbolAddress(void* handle, void* address) { + if (handle == nullptr || address == nullptr) { + return false; + } + + Dl_info info = {}; + if (::dladdr(address, &info) == 0 || info.dli_fbase == nullptr) { + return false; + } + + link_map* map = nullptr; + if (::dlinfo(handle, RTLD_DI_LINKMAP, &map) != 0 || map == nullptr) { + return false; + } + + return reinterpret_cast(map->l_addr) == info.dli_fbase; +} + SymbolResolveResult ResolveSymbolForBionic(const char* name, bool has_window, void* real_gles_handle, const std::vector& stub_handles) { @@ -2677,8 +2701,8 @@ SymbolResolveResult ResolveSymbolForBionic(const char* name, bool has_window, } for (void* h : stub_handles) { - result.address = ::dlsym(h, name); - if (result.address != nullptr) { + if (void *candidate = ::dlsym(h, name); candidate != nullptr && StubOwnsSymbolAddress(h, candidate)) { + result.address = candidate; result.source = SymbolResolveSource::kStub; return result; } @@ -30398,6 +30422,32 @@ void* DelayedSendGameLoadedThread(void* arg) { return nullptr; } +// Resolve the exact Android Vulkan loader adapter shipped next to this +// runtime binary. Never resolve it by SONAME: an unversioned host +// libvulkan.so on the search path (for example distribution or Nix wrappers) +// would win, silently drop VK_KHR_android_surface support, and break +// direct-Vulkan mode with "Unable to create Vulkan instance". +static std::string RuntimeVulkanAdapterPath() { + const char* override_dir = std::getenv("MOCKTAIL_RUNTIME_LIBRARY_DIR"); + std::string directory; + if (override_dir != nullptr && override_dir[0] != '\0') { + directory = override_dir; + } else { + char executable_path[4097] = {}; + const ssize_t length = + ::readlink("/proc/self/exe", executable_path, + sizeof(executable_path) - 1U); + if (length <= 0) return {}; + executable_path[length] = '\0'; + std::string path(executable_path); + const std::string::size_type separator = path.find_last_of('/'); + if (separator == std::string::npos) return {}; + directory = separator == 0 ? "/" : path.substr(0, separator); + } + if (directory.empty()) return {}; + return directory + "/libvulkan.so"; +} + void* EngineStartupThread(void* arg) { auto* context = static_cast(arg); if (context == nullptr) { @@ -32267,17 +32317,25 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options, void* bionic_vulkan_adapter_handle = nullptr; for (const char* name : stub_names) { void* h = nullptr; + bool exact_adapter = false; if (std::strcmp(name, "libEGL.so") == 0 && bionic_egl_bridge.IsLoaded()) { h = bionic_egl_bridge.handle(); - } else { + exact_adapter = true; + } else if (std::strcmp(name, "libvulkan.so") == 0) { + const std::string vulkan_adapter = RuntimeVulkanAdapterPath(); + if (!vulkan_adapter.empty()) { + h = ::dlopen(vulkan_adapter.c_str(), RTLD_LAZY | RTLD_GLOBAL); + exact_adapter = h != nullptr; + } + } + if (h == nullptr) { h = ::dlopen(name, RTLD_LAZY | RTLD_GLOBAL | RTLD_NOLOAD); if (!h) h = ::dlopen(name, RTLD_LAZY | RTLD_GLOBAL); } if (h) { std::cout << " [stubs] Preloaded " << name; - if (std::strcmp(name, "libEGL.so") == 0 && - bionic_egl_bridge.IsLoaded()) { + if (exact_adapter) { std::cout << " via exact Mocktail adapter"; } std::cout << '\n'; @@ -32806,7 +32864,8 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options, size_t vulkan_exports = 0; for (const char* name : kVulkanAdapterExports) { void* address = ::dlsym(bionic_vulkan_adapter_handle, name); - if (address == nullptr) { + if (address == nullptr || + !StubOwnsSymbolAddress(bionic_vulkan_adapter_handle, address)) { continue; } linker::RegisterSyntheticSymbol("libvulkan.so", name, address); diff --git a/src/update/readiness_canary.cc b/src/update/readiness_canary.cc index 8fcdcdf..3244cf5 100644 --- a/src/update/readiness_canary.cc +++ b/src/update/readiness_canary.cc @@ -318,6 +318,14 @@ CanaryResult RunReadinessCanary(const CanaryOptions& options) { "__NV_PRIME_RENDER_OFFLOAD", "__VK_LAYER_NV_optimus", "__GLX_VENDOR_LIBRARY_NAME", + // Dynamic-library search state and graphics library overrides + // must reach the isolated runtime so that distributions which + // resolve shared libraries through the launcher environment + // (for example NixOS wrappers) still find libGLESv2/libEGL. + "LD_LIBRARY_PATH", + "MOCKTAIL_EGL_LIBRARY", + "MOCKTAIL_GLES_LIBRARY", + "MOCKTAIL_ANGLE_LIB_DIR", }) { AddInherited(name, &environment); }