Theo's Blog

Published

- 5 min read

Making Minecraft mods on NixOS

img of Making Minecraft mods on NixOS

Intro

I’ve been making Minecraft mods (and plugins!) for a while (for around 4 years!) now and about 2 years ago I started using NixOS as my main Linux distro.

So… what’s the problem?

When running the beloved “Minecraft Client” task I was met with an unpleasant surprise. That being a GL error.

Here are some of them:

  • java.lang.IllegalStateException: Failed to initialize GLFW, errors: GLFW error during init: [0x1000E]*a bunch of numbers*
  • java.lang.UnsatisfiedLinkError: Failed to locate library: libGL.so.1
  • (maybe more that I couldn’t find)

Basic Setup

I got a good ol’ flake.nix

   {
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    flake-parts.url = "github:hercules-ci/flake-parts";
    systems.url = "github:nix-systems/default";
  };

  outputs = inputs:
    inputs.flake-parts.lib.mkFlake { inherit inputs; } {
      systems = import inputs.systems;
      perSystem = { config, self', pkgs, lib, system, ... }: {
      devShells.default = pkgs.mkShell {
        buildInpts = with pkgs; [
          zulu17
        ];
        LD_LIBRARY_PATH = with pkgs; lib.makeLibraryPath [
          libGL
          glfw
          openal
          flite
          libpulseaudio
          udev
          xorg.libXcursor
        ];
      };
    };
  };
}

and direnv (.envrc)

   use flake

That worked. Until it didn’t.

That allowed me to start the game via terminal and ./gradlew runClient. Which worked, but wasn’t ideal since I wanted to utilize IntelliJ IDEA’s hotreload and debugging tools!

The REAL way of fixing it

After some experimentation I came up with this flake:

   {
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    flake-parts.url = "github:hercules-ci/flake-parts";
    systems.url = "github:nix-systems/default";
  };

  outputs = inputs:
    inputs.flake-parts.lib.mkFlake { inherit inputs; } {
      systems = import inputs.systems;
      perSystem = { config, self', pkgs, lib, system, ... }: let
        libs = with pkgs; [
          # GL
          libGL
          glfw-wayland-minecraft

          # audio
          libpulseaudio
          openal

          # flite <- uncomment to get TTS
        ];
        jbr = pkgs.callPackage ./scripts/jbr.nix {};
      in {
        devShells.default = pkgs.mkShell {
          nativeBuildInputs = with pkgs; [
            jbr
          ];

          buildInputs = libs;
          LD_LIBRARY_PATH = lib.makeLibraryPath libs;

          env = {
           JAVA_HOME = "${jbr}/lib/openjdk/";
          };
        };
      };
    };
}

As you see I use a special Java package. I use JBR (JetBrains Runtime) as it has DCEVM with good debug capabilities (hot reload).

Here’s the said package’s content, which was copied from nix-pkgs:

   { lib
, stdenv
, fetchFromGitHub
, jetbrains
, openjdk17
, openjdk17-bootstrap
, git
, autoconf
, unzip
, rsync

, libXdamage
, libXxf86vm
, libXrandr
, libXi
, libXcursor
, libXrender
, libX11
, libXext
, libxcb
, nss
, nspr
, libdrm
, mesa
, wayland
, udev
}:

let
  arch = {
    "aarch64-linux" = "aarch64";
    "x86_64-linux" = "x64";
  }.${stdenv.hostPlatform.system} or (throw "Unsupported system: ${stdenv.hostPlatform.system}");
  cpu = stdenv.hostPlatform.parsed.cpu.name;
in
openjdk17.overrideAttrs (oldAttrs: rec {
  pname = "jetbrains-jdk";
  javaVersion = "17.0.11";
  build = "1207.24";
  # To get the new tag:
  # git clone https://github.com/jetbrains/jetbrainsruntime
  # cd jetbrainsruntime
  # git reset --hard [revision]
  # git log --simplify-by-decoration --decorate=short --pretty=short | grep "jbr-" --color=never | cut -d "(" -f2 | cut -d ")" -f1 | awk '{print $2}' | sort -t "-" -k 2 -g | tail -n 1 | tr -d ","
  openjdkTag = "jbr-17.0.8+7";
  version = "${javaVersion}-b${build}";

  src = fetchFromGitHub {
    owner = "JetBrains";
    repo = "JetBrainsRuntime";
    rev = "jb${version}";
    hash = "sha256-a7cJF2iCW/1GK0/GmVbaY5pYcn3YtZy5ngFkyAGRhu0=";
  };

  BOOT_JDK = openjdk17-bootstrap.home;
  # run `git log -1 --pretty=%ct` in jdk repo for new value on update
  SOURCE_DATE_EPOCH = 1715809405;

  patches = [ ];

  dontConfigure = true;

  buildPhase = ''
    runHook preBuild

    sed \
        -e "s/OPENJDK_TAG=.*/OPENJDK_TAG=${openjdkTag}/" \
        -e "s/SOURCE_DATE_EPOCH=.*//" \
        -e "s/export SOURCE_DATE_EPOCH//" \
        -i jb/project/tools/common/scripts/common.sh
    sed -i "s/STATIC_CONF_ARGS/STATIC_CONF_ARGS \$configureFlags/" jb/project/tools/linux/scripts/mkimages_${arch}.sh
    sed \
        -e "s/create_image_bundle \"jb/#/" \
        -e "s/echo Creating /exit 0 #/" \
        -i jb/project/tools/linux/scripts/mkimages_${arch}.sh

    patchShebangs .
    ./jb/project/tools/linux/scripts/mkimages_${arch}.sh ${build} nomod

    runHook postBuild
  '';

  installPhase =
    let
      buildType = "release";
      jbrsdkDir = "jbrsdk-${javaVersion}-linux-${arch}-b${build}";
    in
    ''
      runHook preInstall

      mv build/linux-${cpu}-server-${buildType}/images/jdk/man build/linux-${cpu}-server-${buildType}/images/${jbrsdkDir}
      rm -rf build/linux-${cpu}-server-${buildType}/images/jdk
      mv build/linux-${cpu}-server-${buildType}/images/${jbrsdkDir} build/linux-${cpu}-server-${buildType}/images/jdk
    '' + oldAttrs.installPhase + "runHook postInstall";

  dontStrip = false;

  postFixup = ''
    # Build the set of output library directories to rpath against
    LIBDIRS="${lib.makeLibraryPath [
      libXdamage libXxf86vm libXrandr libXi libXcursor libXrender libX11 libXext libxcb
      nss nspr libdrm mesa wayland udev
    ]}"
    for output in $outputs; do
      if [ "$output" = debug ]; then continue; fi
      LIBDIRS="$(find $(eval echo \$$output) -name \*.so\* -exec dirname {} \+ | sort -u | tr '\n' ':'):$LIBDIRS"
    done
    # Add the local library paths to remove dependencies on the bootstrap
    for output in $outputs; do
      if [ "$output" = debug ]; then continue; fi
      OUTPUTDIR=$(eval echo \$$output)
      BINLIBS=$(find $OUTPUTDIR/bin/ -type f; find $OUTPUTDIR -name \*.so\*)
      echo "$BINLIBS" | while read i; do
        patchelf --set-rpath "$LIBDIRS:$(patchelf --print-rpath "$i")" "$i" || true
        patchelf --shrink-rpath "$i" || true
      done
    done
  '';

  nativeBuildInputs = [ git autoconf unzip rsync ] ++ oldAttrs.nativeBuildInputs;

  meta = with lib; {
    description = "An OpenJDK fork to better support Jetbrains's products.";
    longDescription = ''
      JetBrains Runtime is a runtime environment for running IntelliJ Platform
      based products on Windows, Mac OS X, and Linux. JetBrains Runtime is
      based on OpenJDK project with some modifications. These modifications
      include: Subpixel Anti-Aliasing, enhanced font rendering on Linux, HiDPI
      support, ligatures, some fixes for native crashes not presented in
      official build, and other small enhancements.
      JetBrains Runtime is not a certified build of OpenJDK. Please, use at
      your own risk.
    '';
    homepage = "https://confluence.jetbrains.com/display/JBR/JetBrains+Runtime";
    inherit (openjdk17.meta) license platforms mainProgram;
    maintainers = with maintainers; [ edwtjo ];

    broken = stdenv.isDarwin;
  };

  passthru = oldAttrs.passthru // {
    home = "${jetbrains.jdk}/lib/openjdk";
  };
})

To finish the set up, open File > Settings > Build System > Gradle. You’ll see a fiel, where you can choose java installation. Make sure to choose JAVA_HOME, which was set by the flake automatically.

When opening the project, run IntelliJ from terminal while in the dev environment. This will make sure that the environment variables are properly passed on.

All of this will work to just double click :runClient in the Gradle tab and voila!

However, if you want to run the “Minecraft Client” run configration, provided by Fabric Loom or Forge Gradle, you’d need to set your Java installation accordinly in File > Project Settings > SDKs.

Outro

I hope this guide helps someone to set up a proper modding dev environment on NixOS and they don’t have to waste time, the most useful resource, on writing mods without hot reload.