Skip to content

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.

nix
{
  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 automatically
  • cross-musl: musl dynamically linked packages; also auto-cross
  • static: 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 .

shell
~ ❯ 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:

nix
{ 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:

Rust

For simple Rust programs, use rustPlatform.buildRustPackage provided by Nix. See user/apps/tests/syscall/gvisor/default.nix.

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