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包,所以这里直接跳过了patchPhase、configurePhase和buildPhase,当然也不会有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下,其中$out是stdenv.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更新:已解决,见上文)
jdk-8u281-linux-x64.tar.gz
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.so。OpenAL可以直接从nixpkgs获取,这里直接通过LD_LIBRARY_PATH和LD_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"
# ...
'';
}

发现依然启动不了,搜索后找到这篇回答。原来LWJGL的getAvailableDisplayModes函数硬编码了对xrandr的调用=_=
差不多的方法,输入参数中添加xrandr,wrapProgram里添加个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
'';
}
然后在beatoraja的default.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作为可选依赖,作为个人仓库就想着咋快咋来了,可能还要再研究一下更加优雅的写法。
エピローグ
完整代码可以看这里,距离这篇文章的第一版已经过去了很久很久,自己可能偷偷做了比较大的重构之类的,也是经过热心网友提醒才发觉文中的实践存在的不少问题,总之以实际仓库为准~