Add Programs / Add Custom Programs!
Thanks to the new userland build system using Nix for management, adding programs has become extremely simple. Below, we'll explain from top to bottom how to add programs to run in DragonOS.
Concepts
In Nix, a software package is a derivation. So, you just need to define your program as a derivation using Nix, and it becomes an installable package.nixpkgs also provides many native packages, and even includes a syntax to help us quickly specify statically compiled/cross-compiled versions of packages without manually specifying the toolchain.
Below, let's first look at how to quickly add a nixpkgs package to DragonOS.
Adding an nixpkgs Package
First, let's look at the part in user/apps/default.nix that defines and references nixpkgs packages.
{
lib,
pkgs,
nixpkgs,
system,
target,
fenix,
testOpt,
}:
# Return a list of app derivations to be copied into the rootfs.
let
cross =
if system == "x86_64-linux" && target == "x86_64" then
pkgs
else if target == "riscv64" then
pkgs.pkgsCross.riscv64
else
import nixpkgs {
localSystem = system;
crossSystem =
if target == "x86_64" then "x86_64-unknown-linux-gnu" else abort "Unsupported target: ${target}}";
};
cross-musl =
if system == "x86_64-linux" && target == "x86_64" then
pkgs.pkgsMusl
else if target == "riscv64" then
pkgs.pkgsCross.riscv64-musl
else
import nixpkgs {
localSystem = system;
crossSystem =
if target == "x86_64" then
"x86_64-unknown-linux-musl"
else
abort "Unsupported target: ${target}-musl}";
};
static =
if system == "x86_64-linux" && target == "x86_64" then
pkgs.pkgsStatic
else if target == "riscv64" then
import nixpkgs {
crossSystem = lib.systems.examples.riscv64-musl;
isStatic = true;
}
else
abort "Unsupported static target: ${target}";
gvisor-syscall-tests = (
pkgs.callPackage ./tests/syscall/gvisor {
inherit fenix system;
installDir = testOpt.syscall.testDir;
version = testOpt.syscall.version;
}
);
dunitest = (
pkgs.callPackage ./tests/dunitest {
inherit fenix system;
installDir = testOpt.dunitest.testDir;
}
);
in
[
(static.busybox.override {
extraConfig = ''
CONFIG_FEATURE_DEFAULT_PASSWD_ALGO "sha512"
CONFIG_FEATURE_EDITING_MAX_LEN 8192
'';
})
static.curl
static.dropbear
cross.glibc
static.strace
static.gdb
# Simple C utilities
(static.callPackage ./about { })
(static.callPackage ./c_unitest { inherit target; })
]
++ lib.optionals (target == "x86_64" && testOpt.syscall.enable) [
# gvisor test case only included on x86_64
gvisor-syscall-tests
# TODO: Add debian libcxx deps or FHS
]
++ lib.optionals (target == "x86_64" && testOpt.dunitest.enable) [
dunitest
]Treat static and cross as ready-made package prefixes (the equivalent of pkgs in other Nix tutorials). They handle dependencies, cross compilation, and static linking for you:
cross: GNU dynamically linked packages; cross compilation is handled automaticallycross-musl: musl dynamically linked packages; also auto-crossstatic: musl statically linked packages; also auto-cross
The packages injected here are statically linked, such as busybox and dropbear. Search for more packages with nix search github:NixOS/nixpkgs/nixos-25.11 <package_name> or https://search.nixos.org/packages?channel=25.11 .
~ ❯ nix search github:NixOS/nixpkgs/nixos-25.11 dropbear
evaluation warning: darwin.iproute2mac has been renamed to iproute2mac
* legacyPackages.x86_64-linux.dropbear (2025.88)
Small footprint implementation of the SSH 2 protocol
evaluation warning: 'dockerfile-language-server-nodejs' has been renamed to 'dockerfile-language-server'
evaluation warning: beets-stable was aliased to beets, since upstream releases are frequent nowadays
evaluation warning: beets-unstable was aliased to beets, since upstream releases are frequent nowadays
evaluation warning: 'f3d' now build with egl support by default, so `f3d_egl` is deprecated, consider using 'f3d' instead.
evaluation warning: beets-stable was aliased to beets, since upstream releases are frequent nowadays
evaluation warning: beets-unstable was aliased to beets, since upstream releases are frequent nowadays
evaluation warning: 'f3d' now build with egl support by default, so `f3d_egl` is deprecated, consider using 'f3d' instead.
evaluation warning: 'hsa-amd-aqlprofile-bin' has been replaced by 'aqlprofile'.
evaluation warning: 'system' has been renamed to/replaced by 'stdenv.hostPlatform.system'
evaluation warning: 'ethersync' has been renamed to 'teamtype'
evaluation warning: Please replace 'pure-lua' with 'moonlight-nvim' as this name was an error
evaluation warning: windows.mingw_w64_pthreads is deprecated, windows.pthreads should be preferred
~ took 28s ❯What is retrieved here is legacyPackages.x86_64-linux.dropbear, indicating that at least this package exists for x86_64. Directly referencing it with cross.dropbear means using this package. Using static.dropbear would rebuild it due to the lack of a remote build cache (but still saves the trouble of manual configuration).
Adding a Custom Package
C/C++
Above, you can also see (static.callPackage ./about {}), where the about package is a custom-built one. Let's see how Nix replaces its Makefile:
{ stdenv }:
stdenv.mkDerivation {
pname = "about";
version = "0.1.0";
src = ./.;
makeFlags = [
"ARCH=x86_64"
"CROSS_COMPILE=${stdenv.cc.targetPrefix}"
];
installPhase = ''
mkdir -p $out/bin
install -m755 about $out/bin/about.elf
'';
meta = {
description = "About utility for DragonOS";
platforms = [ "x86_64-linux" ];
};
}You can also use Nix to package software that is not yet in NixOS (uncommon, especially for non-GUI programs).
More references:
- https://book.divnix.com/ch06-01-simple-c-program.html
- https://ryantm.github.io/nixpkgs/stdenv/stdenv/
- https://wiki.nixos.org/wiki/C
Rust
For simple Rust programs, use rustPlatform.buildRustPackage provided by Nix. See user/apps/tests/syscall/gvisor/default.nix.
{ lib, pkgs, fenix, system, installDir }:
let
fenixPkgs = fenix.packages.${system};
toolchain = fenixPkgs.combine (with fenixPkgs; [
minimal.rustc
minimal.cargo
]);
rustPlatform = pkgs.makeRustPlatform {
cargo = toolchain;
rustc = toolchain;
};
runner = rustPlatform.buildRustPackage {
pname = "gvisor-test-runner-bin";
version = "0.1.0";
src = ./runner;
cargoLock = {
lockFile = ./runner/Cargo.lock;
};
# You can install the binary somewhere other than bin.
postInstall = ''
mkdir -p $out/${installDir}
if [ -f "$out/bin/runner" ]; then
mv "$out/bin/runner" "$out/${installDir}/gvisor-test-runner"
# Clean up empty bin directory if it exists, to avoid clutter in symlinkJoin
rmdir "$out/bin" || true
fi
'';
};
...For complex applications and cross-compilation, you can refer to a few examples from fenix:
TODO: Multiplatform Rust Application