Nix打包初探(以beatoraja为例)

Prolog

这两天从ArchLinux转到了NixOS,一开始只是简单的尝试,结果体验了一回Nix配置管理的强大之后就再也回不去,索性作为主力系统使用了。(Declarative大法好~)

常见的软件都能在nixpkgs里找到,但是不幸常玩的beatoraja不在其列,咱又不能想玩的时候再切回Arch,于是参考了一些教程,自己动手尝试打包了一下。

大致情况

首先,beatoraja依赖于Java 8,有两种选择,一种是使用OpenJDK,额外需要编译一个OpenJFX,而这玩意在NixOS下的编译花了一天也没整明白,只得退而选择unfree但自带JFX的OracleJDK。

2026-01-01 更新:整明白了,用 OpenJDK override 一下 enableJavaFX = true 就行

具体步骤

以下步骤很大程度学习了这篇大佬的文章,讲的超级好QwQ

0. 创建自己的包仓库

这里直接使用了NUR的模板进行创建,完了之后在flake.nix里添加下面内容

{
  inputs.myRepo = {
    url = "github:CrackTC/nur-packages"; # 替换为自己的仓库地址
    inputs.nixpkgs.follows = "nixpkgs";
  };

  outputs = { nixpkgs, ... } @inputs:
    let
      pkgs = import inputs.nixpkgs {
        inherit system;
        config.allowUnfree = true;
      };

      # ...

      myRepo = import inputs.myRepo { inherit pkgs; }; # oraclejdk需要allowUnfree
      extraRepos = {
        inherit myRepo;
      };
    in
    {
      nixosConfigurations.cno = {
        specialArgs = { inherit pkgs extraRepos; };
        modules = [ ... ];
      };
    };
}

这样modules里就可以像下面这样使用自己的仓库里的包啦

{ extraRepos, ... }: {
  environment.systemPackages = with extraRepos; [ myRepo.beatoraja ];
}

1. 创建包

照着模板给的example-package,在pkgs目录下创建beatoraja目录,然后创建default.nix,内容如下

{ stdenv
}:

let
  pname = "beatoraja-modernchic";
  version = "0.8.8";
  fullName = "beatoraja${version}-modernchic";
in
stdenv.mkDerivation rec {
  inherit pname version;
  name = "${pname}-${version}";
}

这里的输入参数会由callPackage根据pkgs自动填充

2. 获取文件

beatoraja的zip文件可以通过https://mocha-repository.info/download/<fullName>.zip获取,其中<fullName>就是上面default.nix里的fullName格式,所以我们需要在default.nix里添加src属性,以及额外用到fetchurl进行下载

{ stdenv
, fetchurl
}:

let
  # ...
in
stdenv.mkDerivation rec {
  # ...
  src = fetchurl {
    url = "https://mocha-repository.info/download/${fullName}.zip";
    sha256 = "..."; # 可以先不填,后面根据报错给的实际值填上
  };
}

2026-01-01 更新:也可以直接用 fetchzip 来获取zip并解压

{ stdenv
, fetchurl
}:

let
  # ...
in
stdenv.mkDerivation rec {
  # ...
  src = fetchurl {
    url = "https://mocha-repository.info/download/${fullName}.zip";
    hash = "..."; # 可以先不填,后面根据报错给的实际值填上
  };
}

3. 打包之Unpack Phase(2026-01-01更新:如果使用 fetchzip 就不需要这一步)

这里需要用到unzip,在buildInputs里添加以获取对unzip包的引用,unpackPhase的内容是这一阶段执行的shell命令,在nativeBuildInputs中添加unzip后就能直接在unpackPhase中使用对应的命令。

{ stdenv
# ...
, unzip
}:

let
  # ...
in
stdenv.mkDerivation rec {
  # ...

  nativeBuildInputs = [ unzip ];
  unpackPhase = "unzip $src"; # $src对应获取到的文件
}

4. 打包之Install Phase

因为直接获取到了JAR包,所以这里直接跳过了patchPhaseconfigurePhasebuildPhase,当然也不会有checkPhase,咱只需要进行安装以及安装前的准备

4.1 安装前准备

这里主要对原有的启动脚本进行一些修改。一方面由于NixOS不遵循FHS,java这类命令自然不能直接用;另一方面,为了保证不变性,输出目录/nix/store是只读的,于是相关的数据文件和目录需要放到$XDG_DATA_HOME下边。

在这一步需要知道java在哪,引入jdk依赖以获取输出路径

{ stdenv
# ...
, jdk}:

let
  # ...
  startupScript = writeShellScript "beatoraja.sh" ''
    export _JAVA_OPTIONS='-Dsun.java2d.opengl=true -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true -Dswing.defaultlaf=com.sun.java.swing.plaf.gtk.GTKLookAndFeel'
    dataDir="''${XDG_DATA_HOME:-$HOME/.local/share}/beatoraja"
    if [ ! -d "$dataDir" ]; then
      mkdir -p "$dataDir"
      cp -r $out/opt/beatoraja/* "$dataDir"
      find "$dataDir" -type f -exec chmod 644 {} \;
      find "$dataDir" -type d -exec chmod 755 {} \;
    fi
    cd "''${XDG_DATA_HOME:-$HOME/.local/share}/beatoraja"
    exec ${jdk.override { enableJavaFX = true; }}/bin/java -Xms1g -Xmx4g -jar $out/opt/beatoraja/beatoraja.jar $@
  '';
in
stdenv.mkDerivation rec {
  # ...
}

4.2 安装

这里将beatoraja.sh移动到$out/bin下,其余文件移动到$out/share/beatoraja下,其中$outstdenv.mkDerivation的输出目录,也就是/nix/store下的包目录,这样就能够在$PATH中找到启动脚本了。

还需要对启动脚本进行一些包装,以便于在启动时添加一些参数,比如-Dsun.java2d.opengl=true,这里使用了makeWrapper,以便在installPhase中使用wrapProgram命令

{ stdenv
# ...
, makeWrapper
}:

let
  # ...
in
stdenv.mkDerivation rec {
  # ...

  nativeBuildInputs = [ ... makeWrapper ];

  preInstall = ''
    rm beatoraja-config.bat
    rm beatoraja-config.command
    rm jportaudio_x64.dll
    rm portaudio_x64.dll
  '';

  installPhase = ''
    runHook preInstall

    mkdir -p $out/opt/beatoraja
    mkdir -p $out/bin
    ln -s ${startupScript} $out/bin/beatoraja
    mv * $out/opt/beatoraja/

    wrapProgram $out/bin/beatoraja \
      --set out $out

    runHook postInstall
  '';
}

完成了之后,别忘了在仓库的default.nix里添加beatoraja的引用~

{ pkgs ? import <nixpkgs> { } }:

{
  # The `lib`, `modules`, and `overlay` names are special
  lib = import ./lib { inherit pkgs; }; # functions
  modules = import ./modules; # NixOS modules
  overlays = import ./overlays; # nixpkgs overlays

  example-package = pkgs.callPackage ./pkgs/example-package { };
  beatoraja = pkgs.callPackage ./pkgs/beatoraja { };
  # some-qt5-package = pkgs.libsForQt5.callPackage ./pkgs/some-qt5-package { };
  # ...
}

5. 遇到的问题及解决方案

5.1 无法自动获取jdk-8u281-linux-x64.tar.gz(2026-01-01更新:已解决,见上文)

oracle官方的下载链接需要登录并同意一个EULA才能获取,这里手动下载了一个放在了自家服务器上,然后对原来的oraclejre8包进行一个override,替换掉src的内容

{ stdenv
# ...
, oraclejre8
}:

let
  jre = oraclejre8.overrideAttrs {
    src = fetchTarball {
      url = "https://static.sora.zip/nix/jdk-8u281-linux-x64.tar.gz";
      sha256 = "...";
    };
  };
in
stdenv.mkDerivation {
  # ...

  preInstall = ''
    # ...

    echo "exec ${jre}/bin/java -Xms1g -Xmx4g -jar '$out/share/beatoraja/beatoraja.jar'" >> ${fullName}/beatoraja.sh

    # ...
  '';

  # ...
}

5.2 处理依赖

这回能见到配置窗口了,但还是启动不了游戏,原因是找不到OpenAL库,准确来说是libopenal.soOpenAL可以直接从nixpkgs获取,这里直接通过LD_LIBRARY_PATHLD_PRELOAD实现动态库的加载。

{ stdenv
# ...
, openal
}:

let
  # ...
in
stdenv.mkDerivation {
  installPhase = ''
    # ...

    wrapProgram $out/bin/beatoraja \
      --set out $out \
      --prefix LD_LIBRARY_PATH : "${openal}/lib" \
      --prefix LD_PRELOAD : "${openal}/lib/libopenal.so"

    # ...
  '';
}

发现依然启动不了,搜索后找到这篇回答。原来LWJGLgetAvailableDisplayModes函数硬编码了对xrandr的调用=_=

差不多的方法,输入参数中添加xrandrwrapProgram里添加个PATH就行了

{ stdenv
# ...
, xrandr
}:

let 
  # ...
in
stdenv.mkDerivation {
  installPhase = ''
    # ...

    wrapProgram $out/bin/beatoraja \
      --prefix PATH : "${xrandr}/bin" \
      --set out $out \
      --prefix LD_LIBRARY_PATH : "${openal}/lib" \
      --prefix LD_PRELOAD : "${openal}/lib/libopenal.so"

    # ...
  '';
}

xrandr的完整包名是xorg.xrandr,输入参数能直接填xrandr,可能callPackage的名称搜索是递归的(?

6. 启动!

改善音频延迟(jportaudio

默认使用的OpenAL在我的设备上音频延迟很不乐观,对于beatoraja这种key音音游来说相当致命,几乎是不可玩的状态。为了获得更低的音频延迟,beatoraja支持使用jportaudio输出,当然这里也是需要咱自己打包的。

jportaudio的官方仓库在这里,由于使用cmake进行构建,打包过程简单多了QwQ

按照同样的步骤在pkg下创建libjportaudio目录,然后创建default.nix,内容如下

{ stdenv
, fetchFromGitHub
, jdk
, cmake      # 使用cmake进行构建
, portaudio  # 依赖libportaudio.so
}:

let
  version = "0.1.0";
  pname = "libjportaudio";
in
stdenv.mkDerivation rec {
  inherit version pname;
  name = "${pname}-${version}";
  src = fetchFromGitHub ({
    owner = "philburk";
    repo = "portaudio-java";
    rev = "ed2d3bc78b42f9c877863618b0ec4dac216102cc";
    sha256 = "sha256-tpJ4JqNFcuDmW70fLa0mW4fytjlU7h77IgMwS3msUX8=";
  });

  nativeBuildInputs = [ cmake portaudio ];

  preConfigure = ''
    export JAVA_HOME=${jdk}
  '';

  # configurePhase会自动执行cmake

  buildPhase = ''
    make jportaudio_0_1_0
  '';

  installPhase = ''
    mkdir -p $out/lib
    cp libjportaudio_0_1_0.so $out/lib/libjportaudio.so
  '';
}

然后在beatorajadefault.nix里添加libjportaudio的引用

{ stdenv
# ...
, libjportaudio
}:

let
  # ...
in
stdenv.mkDerivation rec {
  # ...

  installPhase = ''
    # ...

    wrapProgram $out/bin/beatoraja \
      --prefix PATH : "${xrandr}/bin" \
      --set out $out \
      --prefix LD_LIBRARY_PATH : "${openal}/lib" \
      --prefix LD_LIBRARY_PATH : "${libjportaudio}/lib" \
      --prefix LD_PRELOAD : "${openal}/lib/libopenal.so" \
      --prefix LD_PRELOAD : "${libjportaudio}/lib/libjportaudio.so"

    # ...
  '';
}

注意这里的libjportaudio并不在pkgs里头,所以callPackage的时候得咱自己传进去

{ pkgs ? import <nixpkgs> { } }:

rec {
  # ...

  beatoraja = pkgs.callPackage ./pkgs/beatoraja { libjportaudio = libjportaudio; }; # 看这里看这里!
  libjportaudio = pkgs.callPackage ./pkgs/libjportaudio { };

  # ...
}

之后就可以开始愉快的玩耍啦~

遗憾

整个打包过程中,最遗憾的是OpenJFX的编译,也不知道是不是姿势不对,但我确实在Java项目的编译上没有任何经验,所以就没有继续尝试了。

另一个遗憾是没有将jportaudio作为可选依赖,作为个人仓库就想着咋快咋来了,可能还要再研究一下更加优雅的写法。

エピローグ

完整代码可以看这里,距离这篇文章的第一版已经过去了很久很久,自己可能偷偷做了比较大的重构之类的,也是经过热心网友提醒才发觉文中的实践存在的不少问题,总之以实际仓库为准~