about summary refs log tree commit diff
diff options
context:
space:
mode:
authoraszlig <aszlig@redmoonstudios.org>2016-08-15 17:39:32 +0200
committeraszlig <aszlig@redmoonstudios.org>2016-08-15 17:39:32 +0200
commitbbfc470e6b2908d933e1b3b5ec8c01b0a8110c06 (patch)
treef6ba226b787f645808caf313283c2fd070fd47ca
parenta4bfae6bc883af74555fe1cb581d719499a3d834 (diff)
pkgs/games: Add Starbound 1.0.5 from HumbleBundle
So far we only had the Steam version of Starbound, but since July 22nd
2016 version 1.0 got released and since then had a few bugfixes already.

The Nix implementation here is based on the Steam version but has quite
some improvements: Instead of wrapping the Starbound client and server
with a shell script which sets up the bootconfig, we now use patchelf to
override specific symbols from external libraries and thus can live
without supplying a -bootconfig arg.

By replacing the sbinit.config in the .rdata section of the binaries
with a magic value (13 times the bell character) we can later match the
magic value during open() and provide a default bootconfig using a
temporary file.

Note that it has to be a temporary file instead of fork() and writing to
a pipe() because Starbound is using lseek() during parsing of JSON.

With all this in place we now have everything Starbound related in
$XDG_DATA_HOME/starbound, which is the main reason for doing the
preloading in the first place.

Starbound without patching expects a file called sbinit.config in the
same directory as the executable and even if we'd supply a bootconfig
via the -bootconfig arg, we can only have static paths in there, because
Starbound doesn't do environment variable expansion within paths in the
bootconfig.

Another difference to the Steam packaging is that we now produce
multiple outputs, the default one plus lib for the preload library and
the assets in its own store path.

Signed-off-by: aszlig <aszlig@redmoonstudios.org>
-rw-r--r--pkgs/games/humblebundle/default.nix1
-rw-r--r--pkgs/games/humblebundle/starbound.nix327
2 files changed, 328 insertions, 0 deletions
diff --git a/pkgs/games/humblebundle/default.nix b/pkgs/games/humblebundle/default.nix
index d0e564b8..1c1e7a21 100644
--- a/pkgs/games/humblebundle/default.nix
+++ b/pkgs/games/humblebundle/default.nix
@@ -23,6 +23,7 @@ let
     megabytepunch = callPackage ./megabytepunch.nix {};
     rocketbirds = callPackage ./rocketbirds.nix {};
     spaz = callPackage ./spaz.nix {};
+    starbound = callPackage ./starbound.nix {};
     swordsandsoldiers = callPackage ./swordsandsoldiers.nix {};
     unepic = callPackage ./unepic.nix {};
   };
diff --git a/pkgs/games/humblebundle/starbound.nix b/pkgs/games/humblebundle/starbound.nix
new file mode 100644
index 00000000..0b6ce1c2
--- /dev/null
+++ b/pkgs/games/humblebundle/starbound.nix
@@ -0,0 +1,327 @@
+{ stdenv, fetchHumbleBundle, unzip, fetchurl, writeText, SDL2, mesa
+, makeDesktopItem
+}:
+
+let
+  binaryDeps = {
+    starbound.deps = [ SDL2 mesa ];
+    starbound.needsBootconfig = true;
+
+    starbound_server.name = "starbound-server";
+    starbound_server.needsBootconfig = true;
+
+    asset_packer.name = "starbound-asset-packer";
+    asset_unpacker.name = "starbound-asset-unpacker";
+
+    dump_versioned_json.name = "starbound-dump-versioned-json";
+    make_versioned_json.name = "starbound-make-versioned-json";
+
+    planet_mapgen.name = "starbound-planet-mapgen";
+    update_tilesets.name = "starbound-update-tilesets";
+  };
+
+  desktopItem = makeDesktopItem {
+    name = "starbound";
+    exec = "starbound";
+    icon = fetchurl {
+      url = "http://i1305.photobucket.com/albums/s544/ClockworkBarber/"
+          + "logo_zps64c4860d.png";
+      sha256 = "11fiiy0vcxzix1j81732cjh16wi48k4vag040vmbhad50ps3mg0q";
+    };
+    comment = "An extraterrestrial sandbox adventure game";
+    desktopName = "Starbound";
+    genericName = "starbound";
+    categories = "Game;";
+  };
+
+  patchBinary = bin: attrs: ''
+    mkdir -p "patched/$(dirname "${bin}")"
+    cp -t "patched/$(dirname "${bin}")" "linux/${bin}"
+    chmod +x "patched/$(basename "${bin}")"
+    ${stdenv.lib.optionalString (attrs.needsBootconfig or false) ''
+      for offset in $(
+        grep -abz '^sbinit\.config''$' "patched/$(basename "${bin}")" \
+          | cut -d: -f1 -z | xargs -0
+      ); do
+        for i in $(seq 13); do printf '\x07'; done \
+          | dd of="patched/$(basename "${bin}")" \
+               obs=1 seek="$offset" \
+               count=13 conv=notrunc
+      done
+      patchelf \
+        --add-needed "$lib/lib/libstarbound-preload.so" \
+        "patched/$(basename "${bin}")"
+    ''}
+    patchelf \
+      --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" \
+      --set-rpath "${stdenv.lib.makeLibraryPath (attrs.deps or [])}" \
+      "patched/$(basename "${bin}")"
+    if ldd "patched/$(basename "${bin}")" | grep -F 'not found' \
+       | grep -v 'libstarbound-preload\.so\|libsteam_api\.so'; then
+      exit 1;
+    fi
+  '';
+
+  preloaderSource = writeText "starbound-preloader.c" ''
+    #define _GNU_SOURCE
+    #include <dlfcn.h>
+    #include <fcntl.h>
+    #include <malloc.h>
+    #include <stdarg.h>
+    #include <stdio.h>
+    #include <stdlib.h>
+    #include <string.h>
+    #include <sys/stat.h>
+    #include <sys/wait.h>
+    #include <unistd.h>
+
+    #define MAGIC "\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07"
+
+    static char *getXdgDataHome(void) {
+      int envlen;
+      char *result;
+      const char *env;
+
+      if ((env = getenv("XDG_DATA_HOME")) != NULL)
+        return strdup(env);
+      if ((env = getenv("HOME")) == NULL)
+        return NULL;
+
+      envlen = strlen(env);
+      if ((result = malloc(envlen + 14)) == NULL)
+        return NULL;
+      strncpy(result, env, envlen);
+      strncpy(result + envlen, "/.local/share", 14);
+      return result;
+    }
+
+    static char *mkJsonString(const char *str) {
+      char *result, *out;
+
+      if ((result = malloc(strlen(str) * 6 + 3)) == NULL)
+        return NULL;
+
+      out = result;
+      *out++ = '"';
+
+      for (; *str != '\0'; str++) {
+        switch (*str) {
+          case '"':  *out++ = '\\'; *out++ = '"'; break;
+          case '\\': *out++ = '\\'; *out++ = '\\'; break;
+          case '\b': *out++ = '\\'; *out++ = 'b'; break;
+          case '\f': *out++ = '\\'; *out++ = 'f'; break;
+          case '\n': *out++ = '\\'; *out++ = 'n'; break;
+          case '\r': *out++ = '\\'; *out++ = 'r'; break;
+          case '\t': *out++ = '\\'; *out++ = 't'; break;
+          default:
+            if (*str >= 0 && *str <= 31) {
+              *out++ = '\\';
+              *out++ = 'u';
+              snprintf(out, 4, "%04x", *str);
+              out += 4;
+            } else {
+              *out++ = *str;
+            }
+            break;
+        }
+      }
+
+      *out++ = '"';
+      *out = 0;
+
+      return result;
+    }
+
+    static char *mkDataDir(const char *dataHome, const char *append) {
+      char *buf, *out;
+      int homeLen = strlen(dataHome);
+      int appendLen = strlen(append);
+
+      if ((buf = malloc(homeLen + appendLen + 2)) == NULL)
+        return NULL;
+
+      snprintf(buf, homeLen + appendLen + 2, "%s/%s", dataHome, append);
+
+      out = mkJsonString(buf);
+      free(buf);
+
+      return out;
+    }
+
+    static int makedirs(const char *path) {
+      char *buf, *p;
+
+      if (strlen(path) == 0)
+        return 1;
+
+      if ((buf = strdup(path)) == NULL)
+        return 1;
+
+      for (p = buf + 1; *p != '\0'; p++) {
+        if (*p != '/') continue;
+        *p = '\0';
+        mkdir(buf, 0777);
+        *p = '/';
+      }
+
+      free(buf);
+      return 0;
+    }
+
+    static int writeSBInit(FILE *sbinit) {
+      char *buf, *dataHome;
+      int homeLen, ret;
+
+      if ((dataHome = getXdgDataHome()) == NULL)
+        return 1;
+
+      homeLen = strlen(dataHome);
+      if ((buf = malloc(homeLen + 12)) == NULL)
+        goto errout;
+      strncpy(buf, dataHome, homeLen);
+      strncpy(buf + homeLen, "/starbound/", 12);
+      ret = makedirs(buf);
+      free(buf);
+      if (ret != 0) goto errout;
+
+      fputs("{\"assetDirectories\":[", sbinit);
+
+      if ((buf = mkJsonString(STARBOUND_ASSET_DIR)) == NULL)
+        goto errout;
+      fputs(buf, sbinit);
+      free(buf);
+
+      fputc(',', sbinit);
+
+      if ((buf = mkDataDir(dataHome, "starbound/mods/")) == NULL)
+        goto errout;
+      fputs(buf, sbinit);
+      free(buf);
+
+      fputs("],\"storageDirectory\":", sbinit);
+
+      if ((buf = mkDataDir(dataHome, "starbound/")) == NULL)
+        goto errout;
+      fputs(buf, sbinit);
+      free(buf);
+
+      fputs("}", sbinit);
+      free(dataHome);
+      return 0;
+    errout:
+      free(dataHome);
+      return 1;
+    }
+
+    static FILE *fakeSBInitHandle = NULL;
+
+    static int fakeSBInit(void) {
+      fakeSBInitHandle = tmpfile();
+      if (writeSBInit(fakeSBInitHandle) != 0) {
+        fclose(fakeSBInitHandle);
+        fakeSBInitHandle = NULL;
+        return -1;
+      }
+      rewind(fakeSBInitHandle);
+      return fileno(fakeSBInitHandle);
+    }
+
+    int open(const char *path, int flags, ...) {
+      va_list ap;
+      mode_t mode;
+      static int (*_open) (const char *, int, mode_t) = NULL;
+
+      if (_open == NULL)
+        _open = dlsym(RTLD_NEXT, "open");
+
+      va_start(ap, flags);
+      mode = va_arg(ap, mode_t);
+      va_end(ap);
+
+      if (strncmp(path, MAGIC, 14) != 0)
+        return _open(path, flags, mode);
+
+      return fakeSBInit();
+    }
+
+    int close(int fd) {
+      int ret;
+      static int (*_close) (int) = NULL;
+
+      if (_close == NULL)
+        _close = dlsym(RTLD_NEXT, "close");
+
+      if (fakeSBInitHandle != NULL) {
+        ret = fclose(fakeSBInitHandle);
+        fakeSBInitHandle = NULL;
+      } else {
+        ret = _close(fd);
+      }
+
+      return ret;
+    }
+
+    int SteamAPI_Init(void) {
+      return 0;
+    }
+  '';
+
+in stdenv.mkDerivation rec {
+  name = "starbound-${version}";
+  version = "1.0.5";
+
+  src = fetchHumbleBundle {
+    name = "starbound-linux-${version}.zip";
+    machineName = "starbound_linux";
+    md5 = "0416fa1ddd6a420644fcf3ec18feb90c";
+  };
+
+  outputs = [ "out" "lib" "assets" ];
+
+  nativeBuildInputs = [ unzip ];
+
+  buildPhase = with stdenv.lib; ''
+    cc -Werror -shared "${preloaderSource}" -o preload.so -ldl -fPIC \
+      -DSTARBOUND_ASSET_DIR="\"$assets\""
+    ${concatStrings (mapAttrsToList patchBinary binaryDeps)}
+    patchelf --remove-needed libsteam_api.so patched/starbound
+  '';
+
+  doCheck = true;
+
+  checkPhase = ''
+    checkFailed=
+    for i in linux/*; do
+      [ -f "$i" ] || continue
+
+      case "$(basename "$i")" in
+        sbinit.config) continue;;
+        *.s[ho]) continue;;
+      esac
+
+      [ ! -e "patched/$(basename "$i")" ] || continue
+
+      echo "Found missing binary $i from the upstream tree."
+      checkFailed=1
+    done
+
+    [ -z "$checkFailed" ]
+  '';
+
+  installPhase = ''
+    install -vsD preload.so "$lib/lib/libstarbound-preload.so"
+
+    ${stdenv.lib.concatStrings (stdenv.lib.mapAttrsToList (bin: attrs: let
+      basename = builtins.baseNameOf bin;
+    in ''
+      install -vD "patched/${basename}" "$out/bin/${attrs.name or basename}"
+    '') binaryDeps)}
+
+    install -m 0644 -vD "${desktopItem}/share/applications/starbound.desktop" \
+      "$out/share/applications/starbound.desktop"
+
+    cp -vr assets "$assets"
+  '';
+
+  dontStrip = true;
+}