Packaging Unity games

I’m looking into packaging Daggerfall Unity. Despite technically being an open-source project, it does run on the proprietary Unity engine, so building the project from source is not a real possibility here.

The Unity player doesn’t seem to rely on anything strange in terms of shared libraries, I only noticed one of the native Mono libraries relying on zlib:

 0x0000000000000001 (NEEDED)             Shared library: [libz.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libdl.so.2]
 0x0000000000000001 (NEEDED)             Shared library: [libpthread.so.0]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000e (SONAME)             Library soname: [libMonoPosixHelper.so]

Other than that, nothing out of the ordinary, so I tried going through the usual motions one would go through when trying to package a binary, aka, just use autoPatchelfHook, which results in

{ stdenv
, lib
, fetchzip
, autoPatchelfHook
, zlib
}:

stdenv.mkDerivation rec {
  pname = "daggerfall-unity";
  version = "0.14.5";

  src = fetchzip {
    url = "https://github.com/Interkarma/daggerfall-unity/releases/download/v0.14.5-beta/dfu_linux_64bit-v${version}-beta.zip";
    sha256 = "8OXIzkyVZgSbDtcB+BreFSHNJqTKP2kNRWgibWwwZtc=";
    stripRoot = false;
  };

  nativeBuildInputs = [
    autoPatchelfHook
  ];

  buildInputs = [
    zlib
  ];

  sourceRoot = ".";

  installPhase = ''
    mkdir -p "$out/bin"
    cp -R source/* "$out/bin"
  '';
}

This of course builds just fine, and patches the zlib path. Trying to run the built package, however, does nothing.

With a quick strace, we can see that, at runtime, the process is trying to open several Xorg libraries:

Openat(AT_FDCWD, "/nix/store/hsk71z8admvgykn7vzjy11dfnar9f4r1-glibc-2.35-163/lib/libX11.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/nix/store/hsk71z8admvgykn7vzjy11dfnar9f4r1-glibc-2.35-163/lib/libXext.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/nix/store/hsk71z8admvgykn7vzjy11dfnar9f4r1-glibc-2.35-163/lib/libXcursor.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/nix/store/hsk71z8admvgykn7vzjy11dfnar9f4r1-glibc-2.35-163/lib/libXinerama.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/nix/store/hsk71z8admvgykn7vzjy11dfnar9f4r1-glibc-2.35-163/lib/libXi.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/nix/store/hsk71z8admvgykn7vzjy11dfnar9f4r1-glibc-2.35-163/lib/libXrandr.so.2", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/nix/store/hsk71z8admvgykn7vzjy11dfnar9f4r1-glibc-2.35-163/lib/libXss.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/nix/store/hsk71z8admvgykn7vzjy11dfnar9f4r1-glibc-2.35-163/lib/libXxf86vm.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)

What I don’t understand is why it ends up looking for those libraries in the glibc store path? Is that the result of patchelf doing something strange?

Using LD_DEBUG=libs ./result/bin/DaggerfallUnity.x86_64 shows that indeed the libraries are searched but never initialized:

 37375:	find library=libX11.so.6 [0]; searching
     37375:	 search cache=/nix/store/hsk71z8admvgykn7vzjy11dfnar9f4r1-glibc-2.35-163/etc/ld.so.cache
     37375:	 search path=/nix/store/hsk71z8admvgykn7vzjy11dfnar9f4r1-glibc-2.35-163/lib		(system search path)
     37375:	  trying file=/nix/store/hsk71z8admvgykn7vzjy11dfnar9f4r1-glibc-2.35-163/lib/libX11.so.6

Etc. etc.

I’m also curious as to why the glibc store path is listed as the “system search path”?

Is there a way to work around this issue? I know using something like steam-run would probably take care of this issue, but I’m curious as to how to solve this issue, and having steam-run as a dependency when trying to package something doesn’t quite seem like the right approach.

If something is linking libraries at runtime (rather than load time), you can explicitly tell patchelf to add it to the load path anyway with runtimeDependencies.

Since there is no “real” global library path in nixos, it’s not so unreasonable that it amounts to a single directory with some glibc stuff. I don’t think that’s wrong.

Yes, sorry, I should have included this but I tried not to put too much info in my original question. I tried setting runtimeDependencies as well, like this:

  runtimeDependencies = [
    xorg.libX11
    xorg.libXcursor
    xorg.libXext
    xorg.libXi
    xorg.libXinerama
    xorg.libXrandr
    xorg.libXScrnSaver
    xorg.libXxf86vm
  ];

However, Daggerfall Unity (and I believe every single Unity game) comes with a main executable (DaggerfallUnity.x86_64 in this case), as well as a generic Unity player shared library.

[nix-shell:~/Source/nixpkgs-dfu]$ ll result/bin
total 38488
dr-xr-xr-x 1 root root      460 Jan  1  1970 DaggerfallUnity_Data
-r-xr-xr-x 1 root root    10368 Jan  1  1970 DaggerfallUnity.x86_64
-r-xr-xr-x 1 root root     7960 Jan  1  1970 LinuxPlayer_s.debug
-r-xr-xr-x 1 root root  7540992 Jan  1  1970 UnityPlayer_s.debug
-r-xr-xr-x 1 root root 31846184 Jan  1  1970 UnityPlayer.so

Indeed, the rpath of the main executable is patched:

[nix-shell:~/Source/nixpkgs-dfu]$ readelf -d result/bin/DaggerfallUnity.x86_64

Dynamic section at offset 0x1dc8 contains 30 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [UnityPlayer.so]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libpthread.so.0]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000e (SONAME)             Library soname: [LinuxPlayer]
 0x000000000000001d (RUNPATH)            Library runpath: [/nix/store/7s72wy4q3sy6p9kvf2hyj7lhmv9cvckg-libX11-1.8.1/lib:/nix/store/ar5q71s83h89vb1svx4bfx3lz11bdpq3-libXcursor-1.2.0/lib:/nix/store/gpa8dhckvkyzw1blng40nhwvv8kpg0km-libXext-1.3.4/lib:/nix/store/2d2ffx6zzyffn5cm0qafvs73bbm1hv48-libXi-1.8/lib:/nix/store/z0k7wwpyl91v4g1zsl8pvj1vw6bqn5va-libXinerama-1.1.4/lib:/nix/store/qk6427azzgb2kdv7958jj71a53gv6x8p-libXrandr-1.5.2/lib:/nix/store/97vcks69cvmn6fk91rwk1v7299sj1f3p-libXScrnSaver-1.2.3/lib:/nix/store/qldalknyrv51a8iqwzd41x4lmvpwya6r-libXxf86vm-1.1.4/lib:/nix/store/p8hiyy1sx37sm9gvnz0z4cachv0lp178-daggerfall-unity-0.14.5/bin]

But the UnityPlayer.so remains unpatched it would appear, despite the fact that the docs seem to indicate that autoPatchelfHook looks recursively for all shared objects.

[nix-shell:~/Source/nixpkgs-dfu]$ readelf -d result/bin/UnityPlayer.so

Dynamic section at offset 0x1e24fb8 contains 31 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libdl.so.2]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [librt.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libpthread.so.0]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [ld-linux-x86-64.so.2]
 0x000000000000000e (SONAME)             Library soname: [UnityPlayer.so]

 // No `Library runpath` entry here.

If I try to set it manually myself, like so:

  nativeBuildInputs = [
    # Disable the hook for a second
    # autoPatchelfHook
  ];

  buildInputs = [
    zlib
  ];

  installPhase = ''
    mkdir -p "$out/bin"
    cp -R source/* "$out/bin"
  '';

  preFixup = let
    libPath = lib.makeLibraryPath [
      xorg.libX11
      xorg.libXcursor
      xorg.libXext
      xorg.libXi
      xorg.libXinerama
      xorg.libXrandr
      xorg.libXScrnSaver
      xorg.libXxf86vm
    ];
  in ''
    patchelf --set-rpath "${libPath}" $out/bin/UnityPlayer.so
  '';

Then I end up with an empty runpath:

[nix-shell:~/Source/nixpkgs-dfu]$ readelf -d result/bin/UnityPlayer.so

Dynamic section at offset 0x1e5f000 contains 32 entries:
  Tag        Type                         Name/Value
 0x000000000000001d (RUNPATH)            Library runpath: []
 0x0000000000000001 (NEEDED)             Shared library: [libdl.so.2]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [librt.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libpthread.so.0]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [ld-linux-x86-64.so.2]
 0x000000000000000e (SONAME)             Library soname: [UnityPlayer.so]

At which point I sort of had to admit I no longer had any idea what was going on…
(Which is undoubtedly the fault of me being an idiot, but I’m still not sure how to proceed either way.)

We’re moving beyond where I feel like I have enough knowledge to help, but I will point out one thing that stands out to me: you put that patchelf command in preFixup, and the normal autoPatchelfHook runs during fixup, so the automatic one is running after yours, and probably overwriting whatever you did.

Yes, the same goes for me, I think I’m a bit over my head here in terms of knowledge. Thanks for the help either way!

I’ll try if moving to another phase has any effect when I get the chance.