List of NixOS Series Posts:
One characteristic of NixOS is that all binary applications and libraries are
stored in /nix/store
directory and managed by Nix package manager. This means
that NixOS doesn't conform to
the FHS standard of Linux,
and there's not even a dynamic library loader like ld-linux-x86-64.so.2
in
/lib
or /lib64
, let alone other shared libraries like libc.so
. Therefore,
unless the program is statically linked, binaries compiled for other Linux
distros will not run on NixOS at all.
Therefore, to use a program not packaged in Nixpkgs yet on NixOS, the best way
is to package it yourself by writing a packaging script in Nix, and add the
package definition to configuration.nix
, in order to install it to the system.
There are three good news and two bad news when it comes to NixOS packaging. The good news are:
- Nixpkgs, ot the software repository for NixOS, provides a ton of functions for automation. For many open source softwares written in popular programming languages (including C/C++, Python, Go, Node.js, Rust, etc., but not Java), you only need to call an existing function and specify the download source of the source code. Nixpkgs will automatically detect the packaging system, pass in correct parameters, and package it for you.
- Nixpkgs also provides existing automated solutions for binary distributed
software (commonly seen in closed-source software):
- One is Autopatchelf, which automatically modifies the library paths in the
binary, and points them to
/nix/store
. - The other is Bubblewrap, or
steam-run
based on Bubblewrap, that can emulate an FHS-compliant environment. As the name suggests,steam-run
mainly focuses on the Steam gaming platform and the games on it, but it can also be used for other closed-source software.
- One is Autopatchelf, which automatically modifies the library paths in the
binary, and points them to
- Nix package manager run the packaging process in an isolated environment. You can think of it as a Docker container, with no networking access, no escalated privileges, and no access to filesystem except a few designated ones. All attempts to access external paths or Internet will fail; the compilation can only proceed with explicitly specified dependencies in the Nix packaging script. Therefore, the packaged program is guaranteed to have no dependency on external files.
And the bad news are:
-
Software developers may not know Linux better than packagers. Developers may hardcode paths in codes and compilation scripts, and make assumptions that are only true in FHS compliant environments. When this happens, you need to write patches and fix the paths yourself, so the program can correctly compile and run on NixOS.
-
When, for some reason, you cannot use an existing function, you need to prepare yourself for a long debugging journey:
- Developer organized the source code in a strange way (like
osdlyrics
), or used a non-standard compilation procedure - Program actively detects its execution environment (like WeChat for UOS)
- Program actively detects changes to itself (like SVP video interpolation software)
- Developer organized the source code in a strange way (like
A few months ago, I replaced my daily driver distro from Arch Linux to NixOS, and I've packaged quite a few programs on NixOS. This post will explain the packaging procedures in NixOS as well as frequent problems and solutions, starting from the easier ones.
Preparation
First, I strongly recommend you to install NixOS and only package on NixOS.
- Although you can package software with Nix on non-NixOS operating systems, the produced software may still have runtime dependencies on the FHS structure, causing incompatibilities on NixOS. Of course, if you're only packaging for yourself, and have no plan to share the packages, you can safely ignore this.
- In addition, you need to use Home-Manager, a program that manages config files in your Home directory with a Nix-language config file, to install Nix-packaged software on a non-NixOS system. You need to do your own research on how to use this program.
Using Packaging Template from NUR
NUR is a Nix software repository managed by individual users, similar to AUR of Arch Linux. NUR provides a ready-to-use template that can be used to centrally manage your packages.
Go to nur-packages-template on GitHub, click "Use this template" and create a repository. You will store all your custom packages in that new repository.
If you want to publish your packages to NUR, you need to send a Pull Request to NUR's main repository and add the URL to your own repository. However, even if you don't do that, you can still use your own repository.
Then, clone your repository.
-
If you don't use Nix Flake, you can run the following command to build the example package from the template:
nix-build -A example-package
-
If you use Flake, you can run the following commands:
nix flake update # Optional, update repositories in flake.lock to latest version nix build ".#example-package"
Then, add your own repository in your NixOS config.
-
If you don't use Nix Flake, add the following definitions to
configuration.nix
:nixpkgs.config.packageOverrides = pkgs: { myRepo = import (builtins.fetchTarball "https://github.com/nix-community/nur-packages-template/archive/master.tar.gz") { inherit pkgs; }; };
Replace
https://github.com/nix-community/nur-packages-template
with your repository URL.Now you can use your own packages in the form of
pkgs.myRepo.example-package
. -
If you use Nix Flake, add the following definitions to the
inputs
section inflake.nix
:inputs = { # ... myRepo = { url = "github:nix-community/nur-packages-template"; inputs.nixpkgs.follows = "nixpkgs"; }; # ... };
Replace
nix-community/nur-packages-template
with your repository URL.Then, in the
output
section inflake.nix
, for each of yournixosConfigurations
definition, add a module for the systems:outputs = { self, nixpkgs, ... }@inputs: { nixosConfigurations."nixos" = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ # Add the following lines at the beginning of modules ({ nixpkgs.overlays = [ (final: prev: { myRepo = inputs.myRepo.packages."${prev.system}"; }) ]; }) # Add the preceding lines at the beginning of modules ./configuration.nix ]; }; };
Now you can use your own packages in the form of
pkgs.myRepo.example-package
.
Add Packages Straight to NixOS Config
Of course, instead of using the template from NUR, you can put your package definitions along with your NixOS config files.
Assuming you have this packaging definition as example-package.nix
: (From
https://github.com/nix-community/nur-packages-template/blob/master/pkgs/example-package/default.nix)
{ stdenv }:
stdenv.mkDerivation rec {
name = "example-package-${version}";
version = "1.0";
src = ./.;
buildPhase = "echo echo Hello World > example";
installPhase = "install -Dm755 example $out";
}
You can use the pkgs.callPackage
function to use it in configuration.nix
:
{ config, pkgs, ... }:
{
# Use this package directly
environment.systemPackages = [
(pkgs.callPackage ./example-package.nix { })
];
# Or define a constant for the package first
environment.systemPackages = let
examplePackage = pkgs.callPackage ./example-package.nix { };
in [
examplePackage
];
}
If you want to build just this package, you can use the following command:
nix-build -E 'with import <nixpkgs> {}; callPackage ./example-package.nix {}'
Phases in Packaging
Although you can
package directly with Nix package manager's builtins.derivation
function,
we usually use the stdenv.mkDerivation
function to generate a Nix package
definition, since it's much easier. Contrary to builtins.derivation
,
stdenv.mkDerivation
splits the packaging process to 7 phases:
-
Unpack phase
-
In this step,
stdenv.mkDerivation
automatically unpacks the source code archive specified bysrc
. For example, if your archive is in.tar.gz
format, it automatically runstar xf
. -
But
stdenv.mkDerivation
doesn't recognize all archive types, for example.zip
. In this case, you need to specify the unpack command yourself:nativeBuildInputs = [ unzip ]; unpackPhase = '' unzip $src '';
-
stdenv.mkDerivation
requires that the source code resides in a top-level folder in the archive. It automaticallycd
s into that folder after unpack.
-
-
Patch phase
- In this step,
stdenv.mkDerivation
applies allpatches
in sequential order. This can be used to fix the incompatibilities between some programs and NixOS.
- In this step,
-
Configure phase
- This is equivalent to running
./configure
orcmake
.stdenv.mkDerivation
automatically detects the packaging system and calls the appropriate commands, or, when no relevant config files exist, automatically skips the phase. - It's worth noting that, to use
cmake
, you need to add an additional line ofnativeBuildInputs = [ cmake ];
to add CMake into the packaging environment. - You can add configuration parameters with
configureFlags
orcmakeFlags
, to enable or disable functionalities of the program.
- This is equivalent to running
-
Build phase
- This is equivalent to running
make
. You can specify the arguments tomake
withmakeFlags
.
- This is equivalent to running
-
Check phase
- This phase executes the unit tests in the source directory, to ensure that the program functions correctly.
- You can skip this step with
doCheck = false;
.
-
Install phase
- This is equivalent to running
make install
, which copies the compilation results to the relevant folder in Nix store. - The whole building process happens in a temporary folder, rather than in Nix store. Therefore, such a copy is necessary.
- When you specify the installation commands yourself, the target path is
stored in variable
$out
.$out
can be either a directory containing files, or simply a file.
- This is equivalent to running
-
Fixup phase
- This step cleans up the results in Nix store by, for example, stripping debug symbols.
- Autopatchelf Hook, a hook that automatically replaces
.so
paths for closed-source programs, is executed in this step. - You can disable this step with
dontFixup = true;
.
For each phase, the command to be executed, or the pre/post command hooks, can be specified. Take the install phase for example:
preInstall = ''
echo Here are the commands executed before installPhase
'';
installPhase = ''
# Run preInstall commands. Included in the default installPhase, but not in your specified one. You need to add this yourself, or preInstall won't run
runHook preInstall
echo Here are the commands for installPhase
# Run postInstall commands, same as above
runHook postInstall
'';
postInstall = ''
echo Here are the commands executed after installPhase
'';
It might be hard to understand the steps just by reading their definitions, so I
will give a few examples with detailed explanations. In addition, I will also
show a few specific functions for popular programming languages, like
buildPythonPackage
for Python and buildGoModule
for Go. All the examples are
from my NUR repository.
Examples: Open Source Software
Packaging open source software tends to be easy, since in the process, Nix
package manager will adjust the environment variables so that the compiler can
find the libraries in other directories in Nix store. Therefore, all generated
binary files are linked to libraries in Nix store, rather than ones in /usr
or
similar paths, so they can be used directly on NixOS. In addition, even if a
hardcoded path appears in the open source software, you can change the path by
creating a patch in the packaging process, to make it work on NixOS.
Easy: LibOQS (C++, CMake, Automated)
Let's take a look at the most simple one: LibOQS. LibOQS provides implementations for various post-quantum cryptography, and can be used for post-quantum support for OpenSSL or BoringSSL.
Since LibOQS is built by CMake and has no dependencies itself, almost all the
work can be automatically done by stdenv.mkDerivations
. All we need to do is
to specify a few extra arguments for CMake:
# When you use pkgs.callPackage, parameters here will be filled with packages from Nixpkgs (if there's a match)
{ lib
, stdenv
, fetchFromGitHub
, cmake
, ...
} @ args:
stdenv.mkDerivation rec {
# Specify package name and version
pname = "liboqs";
version = "0.7.1";
# Download source code from GitHub
src = fetchFromGitHub ({
owner = "open-quantum-safe";
repo = "liboqs";
# Commit or tag, note that fetchFromGitHub cannot follow a branch!
rev = "0.7.1";
# Download git submodules, most packages don't need this
fetchSubmodules = false;
# Don't know how to calculate the SHA256 here? Comment it out and build the package
# Nix will raise an error and show the correct hash
sha256 = "sha256-m20M4+3zsH40hTpMJG9cyIjXp0xcCUBS+cCiRVLXFqM=";
});
# Parallel building, drastically speeds up packaging, enabled by default.
# You only want to turn this off for one of the rare packages that fails with this.
enableParallelBuilding = true;
# If you encounter some weird error when packaging CMake-based software, try enabling this
# This disables some automatic fixes applied to CMake-based software
dontFixCmake = true;
# Add CMake to the building environment, to generate Makefile with it
nativeBuildInputs = [ cmake ];
# Arguments to CMake that controls functionalities of liboqs
cmakeFlags = [
"-DBUILD_SHARED_LIBS=ON"
"-DOQS_BUILD_ONLY_LIB=1"
"-DOQS_USE_OPENSSL=OFF"
"-DOQS_DIST_BUILD=ON"
];
# stdenv.mkDerivation automatically does the rest for you
}
Then run the following command. Nix package manager will build the package
automatically, and link the output to results
in the current directory.
nix-build -E 'with import <nixpkgs> {}; callPackage ./liboqs.nix {}'
Medium: OpenSSL OQS Provider (C, Has Dependencies)
With LibOQS ready, we can package OpenSSL OQS Provider, an encryption/decryption engine for OpenSSL 3.0 that adds post-quantum cryptography.
{ lib
, stdenv
, fetchFromGitHub
, cmake
, liboqs
, openssl_3_0
, python3
, ...
} @ args:
stdenv.mkDerivation rec {
pname = "openssl-oqs-provider";
version = "ec60cde5cc894814016f821a1162fe1a4b888a75";
src = fetchFromGitHub ({
owner = "open-quantum-safe";
repo = "oqs-provider";
rev = "ec60cde5cc894814016f821a1162fe1a4b888a75";
fetchSubmodules = false;
sha256 = "sha256-NyT5CpQeclSJ0b4Qr4McAJXwKgy6SWiUijkAgu6TTNM=";
});
enableParallelBuilding = true;
dontFixCmake = true;
# nativeBuildInputs specifies packages used only during packaging rather than execution
# Like CMake for generating Makefile, and Python for generating config files
nativeBuildInputs = [
cmake
# Add Python and a few packages, to be used by preConfigure
(python3.withPackages (p: with p; [ jinja2 pyyaml tabulate ]))
];
# buildInputs specifies packages used during execution
buildInputs = [
liboqs
openssl_3_0
];
# Commands run before the configure phase, enable all post-quantum algorithms
preConfigure = ''
cp ${sources.openssl-oqs.src}/oqs-template/generate.yml oqs-template/generate.yml
sed -i "s/enable: false/enable: true/g" oqs-template/generate.yml
LIBOQS_SRC_DIR=${sources.liboqs.src} python oqs-template/generate.py
'';
cmakeFlags = [ "-DCMAKE_BUILD_TYPE=Release" ];
# Manually specified installation commands, copy oqsprovider.so to $out/lib
# Usually executables reside in $out/bin, libraries in $out/lib, and application menus in $out/share
# That's not a requirement. You can put them wherever you want in $out
# But it could be more difficult to use them from other places
installPhase = ''
mkdir -p $out/lib
install -m755 oqsprov/oqsprovider.so "$out/lib"
'';
}
This package mainly demonstrates the difference between nativeBuildInputs
and
buildInputs
:
nativeBuildInputs
are only used during packaging. They're usually used to generate config files or compilation scripts. During cross compilation (compiling for a device of another architecture),nativeBuildInputs
will have the same architecture as the device running the build, rather than the target device. For example, if you're building for ARM Raspberry Pi on a x86 PC,nativeBuildInputs
will have architecture x86.buildInputs
are used both in packaging and in program execution. All dependent libraries go in here. These dependencies have the same architecture as the target device. As an example,liboqs
required byopenssl-oqs-provider
must have the same architecture (both x86 or both ARM).
Hard: OSDLyrics (Python & C++, Two-Stage Build)
Next, let's take a look at OSDLyrics, a desktop lyrics software. On the first glance, this software is easy to package, as the official installation instruction is only four lines:
./autogen.sh
./configure --prefix=/usr PYTHON=/usr/bin/python3
make
sudo make install
However, things become more difficult as Python is involved in compilation.
OSDLyrics consists of two parts, Python and C++, and the C++ part will call the
Python libraries. As the result, the official installation script will copy the
Python module to Python's site-packages
directory. But since Python's
installation directory is read only for OSDLyrics on Nix, the installation
cannot proceed.
Therefore, we need to package the Python module independently:
{ python3Packages
, fetchFromGitHub
, writeText
, ...
}:
python3Packages.buildPythonPackage rec {
pname = "osdlyrics";
version = "0.5.10";
src = fetchFromGitHub ({
owner = "osdlyrics";
repo = "osdlyrics";
rev = "0.5.10";
fetchSubmodules = false;
sha256 = "sha256-x9gIT1JkfPIc4RmmQJLv9rOG2WqAftoTK5uiRlS65zU=";
});
configurePhase =
let
# The original Python module doesn't comply with PIP's packaging standards
# Need to add these two config files
setupPy = writeText "setup.py" ''
from setuptools import setup, find_packages
setup(
name='${pname}',
version='${version}',
packages=['osdlyrics', 'osdlyrics/dbusext'],
)
'';
initPy = writeText "__init__.py" ''
PROGRAM_NAME = 'OSD Lyrics'
PACKAGE_NAME = '${pname}'
PACKAGE_VERSION = '${version}'
'';
in
# Rename the Python module folder & add configs to adhere to standards
''
ln -s ${setupPy} setup.py
mv python osdlyrics
ln -s ${initPy} osdlyrics/__init__.py
'';
# Disable tests, there aren't any in the source code
doCheck = false;
}
Then add the module to the Python environment used by OSDLyrics:
{ python3Packages
, fetchFromGitHub
, writeText
, python3
, ...
}:
let
osdlyricsPython = python3Packages.buildPythonPackage rec {
# ...
};
# These packages are needed by OSDLyrics
python = python3.withPackages (p: with p; [
chardet
dbus-python
future
mpd2
osdlyricsPython
pycurl
pygobject3
]);
in
# ...
Finally, package its C++ part:
{ ... }:
let
# ...
in
stdenv.mkDerivation rec {
pname = "osdlyrics";
version = "0.5.10";
src = fetchFromGitHub ({
owner = "osdlyrics";
repo = "osdlyrics";
rev = "0.5.10";
fetchSubmodules = false;
sha256 = "sha256-x9gIT1JkfPIc4RmmQJLv9rOG2WqAftoTK5uiRlS65zU=";
});
nativeBuildInputs = [
# Automatically run autoconf, same as autogen.sh
autoreconfHook
# Tool to generate language files
intltool
# pkgconfig, used by autoconf scripts to search for dependencies
pkg-config
];
# Dependencies of C++ part
buildInputs = [
dbus-glib
gtk2
libnotify
# Note that this Python is the one defined above, with a few extra modules
python
];
# Fix some compilation errors
postPatch = ''
sed -i 's/-Werror//g' configure.ac
'';
# autoreconfHook adds an autoreconf phase to packaging, with its own pre/post hooks
preAutoreconf = ''
export AUTOPOINT=intltoolize
'';
# Use the Python with extra modules
makeFlags = [ "PYTHON=${python}/bin/python" ];
# Remove Python modules from output (already packaged)
postInstall = ''
rm -rf $out/lib/python*
'';
}
The final complete definition is:
{ stdenv
, lib
, fetchFromGitHub
, writeText
, python3Packages
# nativeBuildInputs
, autoreconfHook
, intltool
, pkg-config
# buildInputs
, dbus-glib
, gtk2
, libnotify
, python3
, ...
} @ args:
let
pname = "osdlyrics";
version = "0.5.10";
src = fetchFromGitHub ({
owner = "osdlyrics";
repo = "osdlyrics";
rev = "0.5.10";
fetchSubmodules = false;
sha256 = "sha256-x9gIT1JkfPIc4RmmQJLv9rOG2WqAftoTK5uiRlS65zU=";
});
osdlyricsPython = python3Packages.buildPythonPackage rec {
inherit pname version src;
configurePhase =
let
setupPy = writeText "setup.py" ''
from setuptools import setup, find_packages
setup(
name='${pname}',
version='${version}',
packages=['osdlyrics', 'osdlyrics/dbusext'],
)
'';
initPy = writeText "__init__.py" ''
PROGRAM_NAME = 'OSD Lyrics'
PACKAGE_NAME = '${pname}'
PACKAGE_VERSION = '${version}'
'';
in
''
ln -s ${setupPy} setup.py
mv python osdlyrics
ln -s ${initPy} osdlyrics/__init__.py
'';
doCheck = false;
};
python = python3.withPackages (p: with p; [
chardet
dbus-python
future
mpd2
osdlyricsPython
pycurl
pygobject3
]);
in
stdenv.mkDerivation rec {
inherit pname version src;
nativeBuildInputs = [
autoreconfHook
intltool
pkg-config
];
buildInputs = [
dbus-glib
gtk2
libnotify
python
];
postPatch = ''
sed -i 's/-Werror//g' configure.ac
'';
preAutoreconf = ''
export AUTOPOINT=intltoolize
'';
makeFlags = [ "PYTHON=${python}/bin/python" ];
postInstall = ''
rm -rf $out/lib/python*
'';
}
Examples: Closed Source Software (& Binary Distributed Ones)
Compared to open source software, packaging closed source ones tend to be more difficult. These closed source software usually distribute only the binary files, which are compiled for those traditional Linux distros adhering to FHS standard directory structures, like CentOS, Debian, Ubuntu, etc. As we don't have the source code, we can only modify the binary files, replacing all the FHS standard paths with ones from Nix store.
Fortunately, Nixpkgs provides a number of schemes for different scenarios, and many closed source software can be packaged successfully.
Easy: Bilibili-linux (Unpack DEB, Electron)
Let's take a look at an easy scenario: Electron based software. Here I use the example of Bilibili-linux, the official Bilibili Windows desktop client ported to Linux.
Although compared to traditional GTK or Qt programs, Electron programs consume more power and disk space, and install dozens of Chromiums in each and every PC, resulting in a market share of over 1000%, their ease of porting should not be ignored. The Bilibili-linux client is implemented in pure Javascript, and there's no binary files in the package, except for Electron. Therefore, we can take its Javascript code, and simply run it with the system-wide Electron.
{ stdenv
, fetchurl
, electron
, lib
, makeWrapper
, ...
} @ args:
################################################################################
# Mostly based on bilibili-bin package from AUR:
# https://aur.archlinux.org/packages/bilibili-bin
################################################################################
stdenv.mkDerivation rec {
pname = "bilibili";
version = "1.2.1-1";
src = fetchurl {
url = "https://github.com/msojocs/bilibili-linux/releases/download/v1.2.1-1/io.github.msojocs.bilibili_1.2.1-1_amd64.deb";
sha256 = "sha256-t/igezm0ipkOkKION8qTYGK9f6qI3c4iPuS/wWrMywQ=";
};
# Unpack DEB package
unpackPhase = ''
ar x ${src}
tar xf data.tar.xz
'';
# makeWrapper generates a command that calls another command (aka wrapper),
# that modifies parameters and environment variables based on the original ones
buildInputs = [ makeWrapper ];
installPhase = ''
mkdir -p $out/bin
# Replace paths in application menu (desktop files)
cp -r usr/share $out/share
sed -i "s|Exec=.*|Exec=$out/bin/bilibili|" $out/share/applications/*.desktop
# Copy out the client's Javascript parts and ignore the rest
cp -r opt/apps/io.github.msojocs.bilibili/files/bin/app $out/opt
# Creates bilibili command that loads the client's Javascript package with electron ($out/opt/app.asar)
makeWrapper ${electron}/bin/electron $out/bin/bilibili \
--argv0 "bilibili" \
--add-flags "$out/opt/app.asar"
'';
}
Medium: DingTalk (Auto Patch Binaries, Finding Dependencies)
Of course, not all closed source software are built with Electron. For the ones
with binary files, we need to modify them by changing all the dependent library
paths to ones in Nix store. Nixpkgs offers an easy-to-use tool called
autoPatchelfHook
, that searches for all the binaries in the package, modifies
them all. It will error out when a dependency isn't met, which is useful for
debugging.
Our example this time will be the Linux client for DingTalk. It uses GTK as its UI framework. Since we have no idea of its dependencies, we first create a packaging template:
{ stdenv
, fetchurl
, autoPatchelfHook
, makeWrapper
, lib
, callPackage
, ...
} @ args:
################################################################################
# Mostly based on dingtalk-bin package from AUR:
# https://aur.archlinux.org/packages/dingtalk-bin
################################################################################
stdenv.mkDerivation rec {
pname = "dingtalk";
version = "1.4.0.20425";
src = fetchurl {
url = "https://dtapp-pub.dingtalk.com/dingtalk-desktop/xc_dingtalk_update/linux_deb/Release/com.alibabainc.dingtalk_${version}_amd64.deb";
sha256 = "sha256-UKkFuuFK/Ae+XIWbPYYsqwS/FOJfOqm9e1i18JB8UfA=";
};
# autoPatchelfHook automatically patches binaries
nativeBuildInputs = [ autoPatchelfHook makeWrapper ];
unpackPhase = ''
ar x ${src}
tar xf data.tar.xz
mv opt/apps/com.alibabainc.dingtalk/files/version version
mv opt/apps/com.alibabainc.dingtalk/files/*-Release.* release
# Remove libraries that can be replaced with system ones, and useless EXEs
rm -rf release/Resources/{i18n/tool/*.exe,qss/mac}
rm -f release/{*.a,*.la,*.prl}
rm -f release/dingtalk_updater
rm -f release/libgtk-x11-2.0.so.*
rm -f release/libm.so.*
'';
installPhase = ''
mkdir -p $out
mv version $out/
# Some libraries must be the same ones from the package
mv release $out/lib
# The desktop file and icon is obtained from AUR
mkdir -p $out/share/applications $out/share/pixmaps
ln -s ${./dingtalk.desktop} $out/share/applications/dingtalk.desktop
ln -s ${./dingtalk.png} $out/share/pixmaps/dingtalk.png
'';
}
Then we try to build the package. It fails, as expected:
# ...
> auto-patchelf failed to find all the required dependencies.
> Add the missing dependencies to --libs or use `--ignore-missing="foo.so.1 bar.so etc.so"`.
For full logs, run 'nix log /nix/store/gm3d0jm6l19ypcz6vfmv5hmx8d9iygr1-dingtalk-1.4.0.20425.drv'.
Let's run the command above to see the complete log:
# ...
error: auto-patchelf could not satisfy dependency libX11.so.6 wanted by /nix/store/w179pb9w545rwnhvv0kkcjvra0gv82sp-dingtalk-1.4.0.20425/lib/cefclient
error: auto-patchelf could not satisfy dependency libgtk-x11-2.0.so.0 wanted by /nix/store/w179pb9w545rwnhvv0kkcjvra0gv82sp-dingtalk-1.4.0.20425/lib/cefclie
nt
error: auto-patchelf could not satisfy dependency libgdk_pixbuf-2.0.so.0 wanted by /nix/store/w179pb9w545rwnhvv0kkcjvra0gv82sp-dingtalk-1.4.0.20425/lib/cefc
lient
error: auto-patchelf could not satisfy dependency libgobject-2.0.so.0 wanted by /nix/store/w179pb9w545rwnhvv0kkcjvra0gv82sp-dingtalk-1.4.0.20425/lib/cefclie
nt
error: auto-patchelf could not satisfy dependency libglib-2.0.so.0 wanted by /nix/store/w179pb9w545rwnhvv0kkcjvra0gv82sp-dingtalk-1.4.0.20425/lib/cefclient
# ...
autoPatchelf
already listed all missing libraries, and we need to find the
relevant packages one by one, and add them to the buildInputs
of the package.
You can search for packages on NixOS Search
based on your experience, or use
nix-index, a tool to search for packages with filenames,
to speed up the process.
After all dependencies are met, the definition for DingTalk looks like:
{ stdenv
, fetchurl
, autoPatchelfHook
, makeWrapper
, lib
, callPackage
# DingTalk dependencies
, alsa-lib
, at-spi2-atk
, at-spi2-core
, cairo
, cups
, dbus
, e2fsprogs
, gdk-pixbuf
, glib
, gnutls
, graphite2
, gtk2
, krb5
, libdrm
, libgcrypt
, libGLU
, libpulseaudio
, libthai
, libxkbcommon
, mesa_drivers
, nspr
, nss
, rtmpdump
, udev
, util-linux
, xorg
, ...
} @ args:
################################################################################
# Mostly based on dingtalk-bin package from AUR:
# https://aur.archlinux.org/packages/dingtalk-bin
################################################################################
let
version = "1.4.0.20425";
# Dingtalk relies on an older version of OpenLDAP
# openldap-2_4.nix can be found in my NUR
openldap = callPackage ./openldap-2_4.nix { };
libraries = [
alsa-lib
at-spi2-atk
at-spi2-core
cairo
cups
dbus
e2fsprogs
gdk-pixbuf
glib
gnutls
graphite2
gtk2
krb5
libdrm
libgcrypt
libGLU
libpulseaudio
libthai
libxkbcommon
mesa_drivers
nspr
nss
openldap
rtmpdump
udev
util-linux
xorg.libICE
xorg.libSM
xorg.libX11
xorg.libxcb
xorg.libXcomposite
xorg.libXcursor
xorg.libXdamage
xorg.libXext
xorg.libXfixes
xorg.libXi
xorg.libXinerama
xorg.libXmu
xorg.libXrandr
xorg.libXrender
xorg.libXScrnSaver
xorg.libXt
xorg.libXtst
];
in
stdenv.mkDerivation rec {
pname = "dingtalk";
inherit version;
src = fetchurl {
url = "https://dtapp-pub.dingtalk.com/dingtalk-desktop/xc_dingtalk_update/linux_deb/Release/com.alibabainc.dingtalk_${version}_amd64.deb";
sha256 = "sha256-UKkFuuFK/Ae+XIWbPYYsqwS/FOJfOqm9e1i18JB8UfA=";
};
nativeBuildInputs = [ autoPatchelfHook makeWrapper ];
buildInputs = libraries;
unpackPhase = ''
ar x ${src}
tar xf data.tar.xz
mv opt/apps/com.alibabainc.dingtalk/files/version version
mv opt/apps/com.alibabainc.dingtalk/files/*-Release.* release
# Cleanup
rm -rf release/Resources/{i18n/tool/*.exe,qss/mac}
rm -f release/{*.a,*.la,*.prl}
rm -f release/dingtalk_updater
rm -f release/libgtk-x11-2.0.so.*
rm -f release/libm.so.*
'';
installPhase = ''
mkdir -p $out
mv version $out/
# Move libraries
# DingTalk relies on (some of) the exact libraries it ships with
mv release $out/lib
# Entrypoint
mkdir -p $out/bin
# Dingtalk dynamically loads libraries during execution, so all dependencies
# should be listed on LD_LIBRARY_PATH so they can be found
makeWrapper $out/lib/com.alibabainc.dingtalk $out/bin/dingtalk \
--argv0 "com.alibabainc.dingtalk" \
--prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath libraries}"
# App Menu
mkdir -p $out/share/applications $out/share/pixmaps
ln -s ${./dingtalk.desktop} $out/share/applications/dingtalk.desktop
ln -s ${./dingtalk.png} $out/share/pixmaps/dingtalk.png
'';
}
Hard: SVP (Integrity Check, Bubblewrap)
Although some closed source software like dingtalk are troublesome to package, requiring manually searching for all dependencies and repeated testing, the software itself will not create more obstacles for you. Other closed source ones, to avoid being cracked, will check their own integrities, and refuse to start if their binary files are ever changed. SVP video interpolation software, for example, is one of them.
autoPatchelfHook
is a no-no for such software. We have to switch to another
way, by creating a FHS-compliant virtual environment, placing all libraries in
the correct paths, and starting the program in this environment. The most
commonly used software for this purpose is
Bubblewrap. It's originally designed
to sandbox programs from sensitive data, but that sandbox can be the virtual
environment we need today.
Let's get straight to the packaging definition of SVP:
{ stdenv
, bubblewrap
, fetchurl
# All dependencies of SVP
, ffmpeg
, glibc
, gnome
, lib
, libmediainfo
, libsForQt5
, libusb1
, lsof
, makeWrapper
, mpv-unwrapped
# NVIDIA driver, SVP needs a library to support accelerated Optical Flow
# Users need to override this to their driver version on systems with NVIDIA
# or set it to null on systems without NVIDIA
, nvidia_x11 ? null
, ocl-icd
, p7zip
, patchelf
, vapoursynth
, wrapMpv
, writeShellScript
, writeText
, xdg-utils
, xorg
, ...
}:
################################################################################
# Based on svp package from AUR:
# https://aur.archlinux.org/packages/svp
################################################################################
let
# Package a MPV with NVIDIA Optical Flow and Vapoursynth video processing engine
mpvForSVP = wrapMpv
(mpv-unwrapped.override {
vapoursynthSupport = true;
})
{
extraMakeWrapperArgs = lib.optionals (nvidia_x11 != null) [
"--prefix"
"LD_LIBRARY_PATH"
":"
"${lib.makeLibraryPath [ nvidia_x11 ]}"
];
};
# Dependencies of the main SVP program
libPath = lib.makeLibraryPath [
libsForQt5.qtbase
libsForQt5.qtdeclarative
libsForQt5.qtscript
libsForQt5.qtsvg
libmediainfo
libusb1
xorg.libX11
stdenv.cc.cc.lib
ocl-icd
vapoursynth
];
# SVP's executable lookup paths (aka PATH environment variable)
execPath = lib.makeBinPath [
ffmpeg.bin
gnome.zenity
lsof
xdg-utils
];
svp-dist = stdenv.mkDerivation rec {
pname = "svp-dist";
version = "4.5.210";
src = fetchurl {
url = "https://www.svp-team.com/files/svp4-linux.${version}-1.tar.bz2";
sha256 = "10q8r401wg81vanwxd7v07qrh3w70gdhgv5vmvymai0flndm63cl";
};
nativeBuildInputs = [ p7zip patchelf ];
# Disable fixup phase, it modifies SVP binary and breaks integrity check
dontFixup = true;
# Decompression and installation steps from AUR: https://aur.archlinux.org/packages/svp-bin
unpackPhase = ''
tar xf ${src}
'';
buildPhase = ''
mkdir installer
LANG=C grep --only-matching --byte-offset --binary --text $'7z\xBC\xAF\x27\x1C' "svp4-linux-64.run" |
cut -f1 -d: |
while read ofs; do dd if="svp4-linux-64.run" bs=1M iflag=skip_bytes status=none skip=$ofs of="installer/bin-$ofs.7z"; done
'';
installPhase = ''
mkdir -p $out/opt
for f in "installer/"*.7z; do
7z -bd -bb0 -y x -o"$out/opt/" "$f" || true
done
for SIZE in 32 48 64 128; do
mkdir -p "$out/share/icons/hicolor/''${SIZE}x''${SIZE}/apps"
mv "$out/opt/svp-manager4-''${SIZE}.png" "$out/share/icons/hicolor/''${SIZE}x''${SIZE}/apps/svp-manager4.png"
done
rm -f $out/opt/{add,remove}-menuitem.sh
'';
};
# Create a startup script with Bubblewrap
startScript = writeShellScript "SVPManager" ''
# Map all paths under root to the virtual environment except these paths
# They're still mapped, but with finer granularity rules
blacklist=(/nix /dev /usr /lib /lib64 /proc)
declare -a auto_mounts
# loop through all directories in the root
for dir in /*; do
# if it is a directory and it is not in the blacklist
if [[ -d "$dir" ]] && [[ ! "''${blacklist[@]}" =~ "$dir" ]]; then
# add it to the mount list
auto_mounts+=(--bind "$dir" "$dir")
fi
done
# Bubblewrap startup scripts
cmd=(
${bubblewrap}/bin/bwrap
# /dev must be mapped with special parameters
--dev-bind /dev /dev
# Switch to the current directory in the virtual environment
--chdir "$(pwd)"
# Kill all processes in virtual environment when Bubblewrap exits
--die-with-parent
# /nix is mapped read-only
--ro-bind /nix /nix
# /proc must be mapped with special parameters
--proc /proc
# Put Glibc to /lib & /lib64 so SVP can load them
--bind ${glibc}/lib /lib
--bind ${glibc}/lib /lib64
# Commands used by SVP, path hardcoded to /usr/bin in SVP
--bind /usr/bin/env /usr/bin/env
--bind ${ffmpeg.bin}/bin/ffmpeg /usr/bin/ffmpeg
--bind ${lsof}/bin/lsof /usr/bin/lsof
# Setup environment variables, including executable and library search paths
--setenv PATH "${execPath}:''${PATH}"
--setenv LD_LIBRARY_PATH "${libPath}:''${LD_LIBRARY_PATH}"
# Map the MPV player packaged for SVP
--symlink ${mpvForSVP}/bin/mpv /usr/bin/mpv
# Map other paths under root
"''${auto_mounts[@]}"
# Run main SVP program once virtual environment starts
${svp-dist}/opt/SVPManager "$@"
)
exec "''${cmd[@]}"
'';
# SVP application menu item
desktopFile = writeText "svp-manager4.desktop" ''
[Desktop Entry]
Version=1.0
Encoding=UTF-8
Name=SVP 4 Linux
GenericName=Real time frame interpolation
Type=Application
Categories=Multimedia;AudioVideo;Player;Video;
MimeType=video/x-msvideo;video/x-matroska;video/webm;video/mpeg;video/mp4;
Terminal=false
StartupNotify=true
Exec=${startScript} %f
Icon=svp-manager4.png
'';
in
# Create a simple package with only startup script and menu item
stdenv.mkDerivation {
pname = "svp";
inherit (svp-dist) version;
phases = [ "installPhase" ];
installPhase = ''
mkdir -p $out/bin $out/share/applications
ln -s ${startScript} $out/bin/SVPManager
ln -s ${desktopFile} $out/share/applications/svp-manager4.desktop
ln -s ${svp-dist}/share/icons $out/share/icons
'';
}
Hard: WeChat-UOS (Environment Check, Steam-run)
Another program that checks its execution environment is WeChat client for UOS. Although it's just an Electron app and should be easy to package, it comes with a library that checks UOS license files. If the check fails, you won't be able to login. Therefore, we still need to create a virtual environment and put the license files in the correct locations, so that we can use WeChat.
Here I demonstrate another simple packaging tool: steam-run
. steam-run
calls
Bubblewrap internally, but as its name suggests, it's originally built for the
Steam client and all the games on Steam, so it includes quite a few commonly
used libraries in its default environment, and therefore can run many closed
source programs.
{ stdenv
, fetchurl
, writeShellScript
, electron
, steam
, lib
, scrot
, ...
} @ args:
################################################################################
# Mostly based on wechat-uos package from AUR:
# https://aur.archlinux.org/packages/wechat-uos
################################################################################
let
version = "2.1.4";
# UOS license files obtained from AUR: https://aur.archlinux.org/packages/wechat-uos
license = stdenv.mkDerivation rec {
pname = "wechat-uos-license";
version = "0.0.1";
src = ./license.tar.gz;
installPhase = ''
mkdir -p $out
cp -r etc var $out/
'';
};
# WeChat package, only keep Javascript and necessary libraries just like Bilibili client
resource = stdenv.mkDerivation rec {
pname = "wechat-uos-resource";
inherit version;
src = fetchurl {
url = "https://home-store-packages.uniontech.com/appstore/pool/appstore/c/com.tencent.weixin/com.tencent.weixin_${version}_amd64.deb";
sha256 = "sha256-V74m+dFK9/f0QoHfvIjk7hyIil6FpV9HGkPqwJLvQhM=";
};
unpackPhase = ''
ar x ${src}
'';
installPhase = ''
mkdir -p $out
tar xf data.tar.xz -C $out
mv $out/usr/* $out/
mv $out/opt/apps/com.tencent.weixin/files/weixin/resources/app $out/lib/wechat-uos
chmod 0644 $out/lib/license/libuosdevicea.so
rm -rf $out/opt $out/usr
# use system scrot
pushd $out/lib/wechat-uos/packages/main/dist/
sed -i 's|__dirname,"bin","scrot"|"${scrot}/bin/"|g' index.js
popd
'';
};
# Create a Steam-run environment with UOS license files and WeChat package
steam-run = (steam.override {
extraPkgs = p: [ license resource ];
runtimeOnly = true;
}).run;
# WeChat startup script
startScript = writeShellScript "wechat-uos" ''
# Currently WeChat cannot display tray icon on NixOS, so when you close the window,
# you never get it back. Kill WeChat if it's running so it can be restarted
wechat_pid=`pidof wechat-uos`
if test $wechat_pid; then
kill -9 $wechat_pid
fi
# Start WeChat in virtual environment with Steam-run
${steam-run}/bin/steam-run \
${electron}/bin/electron \
${resource}/lib/wechat-uos
'';
in
# Create a simple package with only startup script and menu item
stdenv.mkDerivation {
pname = "wechat-uos";
inherit version;
phases = [ "installPhase" ];
installPhase = ''
mkdir -p $out/bin $out/share/applications
ln -s ${startScript} $out/bin/wechat-uos
ln -s ${./wechat-uos.desktop} $out/share/applications/wechat-uos.desktop
ln -s ${resource}/share/icons $out/share/icons
'';
}
While steam-run
is handy, it includes a lot of libraries to support the vast
number of Steam games. Using steam-run
for simple programs is putting fine
timber to petty use. I recommend you to package simple programs with Bubblewrap,
and only handle the larger, complicated ones with steam-run
.
Examples: Special Packages
Here close to the end, I will demonstrate packaging some special stuff.
Font: Hoyo-Glyphs
Fonts are packages in NixOS. You just need to place the TTF files in the
package's $out/share/fonts/opentype
folder.
Here I use Hoyo-Glyphs for demonstration. It's a font project created by miHoYo game lovers, that imitates the constructed scripts in miHoYo games, including Genshin Impact, Star Rail, and ZZZ.
{ stdenvNoCC
, lib
, fetchFromGitHub
, ...
} @ args:
# stdenvNoCC is a packaging environment without compilers; we don't need them for fonts
stdenvNoCC.mkDerivation rec {
pname = "hoyo-glyphs";
version = "b2bf17cd3d9637fbf55c23bf46fe380e4f7e0739";
src = fetchFromGitHub ({
owner = "SpeedyOrc-C";
repo = "Hoyo-Glyphs";
rev = "b2bf17cd3d9637fbf55c23bf46fe380e4f7e0739";
fetchSubmodules = false;
sha256 = "sha256-7Jx/7z3QxAi7lsV3JFwUDWJUpaKOmfZyGKL3MUrUopw=";
});
# Find all OTF font files and copy them to $out/share/fonts/opentype
# We do this because fonts are scattered in directories in hoyo-glyphs
installPhase = ''
mkdir -p $out/share/fonts/opentype/
cp font/**/*.otf $out/share/fonts/opentype/
'';
}
Finally, add the package to the font configuration of NixOS to use them:
let
hoyo-glyphs = pkgs.callPackage ./hoyo-glyphs.nix { };
in
{
fonts.fonts = [
hoyo-glyphs
];
}
Go Package: Konnect
The next thing I'll demonstrate is packaging Go programs. Nixpkgs provides a
buildGoModule
function that packages Go programs almost completely
automatically. However, there remains one issue with buildGoModule
: since Go
building process involves downloading dependencies in vendor
directory from
the Internet, buildGoModule
will calculate the hash of the whole vendor
directory, which must be specified on packaging.
Don't know how to calculate the hash here? Comment it out (or change a few characters) and build the package, and Nix will raise an error and show the correct hash.
Here I'm demonstrating with Konnect, an OpenID SSO service with LDAP backend support.
{ fetchFromGitHub
, buildGoModule
}:
buildGoModule rec {
pname = "konnect";
version = "v0.34.0";
src = fetchFromGitHub ({
owner = "Kopano-dev";
repo = "konnect";
rev = "v0.34.0";
fetchSubmodules = false;
sha256 = "sha256-y7SD+czD/jK/m0LbFq7qGjwJgBIXfTNrdsA3pzgD2xE=";
});
vendorSha256 = "sha256-ZrwFUZDTbJx5qvloVOa5qK1ykKNkUn1hjfz0xf+8sWk=";
}
You don't need to specify any compilation commands, as buildGoModule
does
everything for you.
Similarly, many popular languages like Python, NodeJS and Rust have their own packaging functions, which can be found on NixOS Wiki.
One notable exception is Java, since the popular Maven build system doesn't support pinning dependencies to a specific version. Dependencies may change between two builds, which violates the requirements of Nix.
Kernel: linux-xanmod-lantian
Finally, I'll show you the steps of customizing the Linux kernel. As usual,
Nixpkgs provides a convenience function called buildLinux
:
{ pkgs
, stdenv
, lib
, fetchFromGitHub
, buildLinux
, ...
} @ args:
let
version = "5.17.14";
release = "1";
in
buildLinux {
inherit stdenv lib version;
src = fetchFromGitHub {
owner = "xanmod";
repo = "linux";
rev = "${version}-xanmod${release}";
sha256 = "sha256-OutD9Z/4LMT1cNmpq5fHaJZzU6iMDoj2N8GXFvXkECY=";
};
# Specify the name of the kernel module directory
# I changed this following modification to CONFIG_LOCALVERSION
modDirVersion = "${version}-xanmod${release}-lantian";
# Load config from config.nix
structuredExtraConfig = import ./config.nix args;
# Patches applied to kernel. Here I include two patches from Nixpkgs,
# and all patches in the patches directory (auto detected)
kernelPatches = [
pkgs.kernelPatches.bridge_stp_helper
pkgs.kernelPatches.request_key_helper
] ++ (builtins.map
(name: {
inherit name;
patch = ./patches + "/${name}";
})
(builtins.attrNames (builtins.readDir ./patches)));
# Only allow building for x86_64
extraMeta.broken = !stdenv.hostPlatform.isx86_64;
}
config.nix
stores your custom kernel configs. Nixpkgs will apply your changes
on top of
the default kernel config of NixOS.
{ lib, ... }:
with lib.kernel;
{
# Specify a string: use freeform
LOCALVERSION = freeform "-lantian";
# Specify a number: also use freeform, and write number as string
LOG_BUF_SHIFT = freeform "12";
# Compile to module: use module
# If your settings conflict with default config, use lib.mkForce to override
TCP_CONG_CUBIC = lib.mkForce module;
# Compile into kernel: use yes
TCP_CONG_BBR = yes;
# Disable: use no
CRYPTO_842 = no;
}
Conclusion
Software packaging has always been difficult. In the packaging process, you often need to consider all dependencies of the software, and try repeatedly whilst adjusting parameters. Compared to other distros, packaging in NixOS (and Nixpkgs) may seem complicated at first, but is actually easy:
- Many repetitive work are automated with functions;
- Packaging is isolated from the main OS, no worries of package breaking for others because of residuals or missing dependencies.
In this post, I demonstrated a few common packaging scenarios, including open source and closed source ones. But since I only shown a limited number of examples, they certainly do not cover all scenarios you may run into, so you're likely required to do your own research:
- NixOS Wiki provides packaging guides for many popular programming languages, as well as special cases (like Qt).
- Nixpkgs itself is a large package repository with definitions for over 80,000 packages, which serves as a reference.
- NUR is package repositories managed by Nix users, similar to AUR.
All packaging examples in this post are from my NUR repository.