Portable outputs ​
A pog definition normally builds a Nix package containing a generated Bash CLI. The same definition can also produce three opt-in, single-file outputs:
toHostScript, a denixified script that uses commands from the hostPATH.toArx, an experimental Linux executable containing the tool's Nix closure.toAppImage, a Linux AppImage containing the tool's Nix closure.
The transformations happen after Pog has rendered, formatted, and checked the final script. Flags, nested commands, help text, cleanup hooks, and script behavior therefore come from one source definition.
Quick start ​
This flake exports one tool as an ordinary Nix package and in all three portable formats:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
pog.url = "github:jpetrucciani/pog";
};
outputs = { nixpkgs, pog, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
overlays = [ pog.overlays.default ];
};
message = pkgs.pog.pog {
name = "message";
description = "read a message from JSON";
runtimeInputs = [ pkgs.jq ];
flags = [
{
name = "uppercase";
short = "u";
description = "uppercase the message";
bool = true;
}
];
arguments = [ "json_file" ];
script = helpers: ''
value="$(${pkgs.jq}/bin/jq -r '.message' "$1")"
if ${helpers.flag "uppercase"}; then
printf '%s\n' "$value" |
${pkgs.coreutils}/bin/tr '[:lower:]' '[:upper:]'
else
printf '%s\n' "$value"
fi
'';
};
in
{
packages.${system} = {
default = message;
message-host = message.toHostScript;
message-arx = message.toArx;
message-appimage = message.toAppImage;
message-appimage-wrapped = message.toAppImage.wrapped;
};
apps.${system} = {
default = {
type = "app";
program = pkgs.lib.getExe message;
};
message-host = message.toHostScript.app;
message-arx = message.toArx.app;
message-appimage = message.toAppImage.app;
};
};
}Build the single-file artifacts:
nix build .#message-host --out-link result-host
nix build .#message-arx --out-link result-arx
nix build .#message-appimage --out-link result-appimageOr run their app companions:
nix run .#message-arx -- input.json
nix run .#message-appimage -- --uppercase input.jsonOn NixOS, the AppImage's wrapped derivation is also directly runnable:
nix run .#message-appimage.wrapped -- --uppercase input.jsonThe host-script app still expects its declared commands to be installed on the host:
nix run .#message-host -- input.jsonChoosing a format ​
| Output | Dependencies at runtime | Supported hosts | Best use |
|---|---|---|---|
| Ordinary package | Nix store closure | Any system supported by the package set | NixOS, development shells, and Nix-managed machines |
toHostScript | Bash 4+, GNU-compatible getopt, and declared host commands | Hosts with compatible shell tooling | Bootstrap scripts, managed fleets, and readable handoff artifacts |
toArx | Embedded Nix closure plus a small set of launcher commands | Linux on the architecture it was built for | Internal distribution where closure fidelity matters most |
toAppImage | Embedded Nix closure, Linux user namespaces, and FUSE or extraction | Linux on the architecture it was built for | A familiar single-file Linux application |
Use toHostScript when the destination already has the required tools and you want a readable script with no Nix store references. Use toAppImage for the most conventional single-file Linux experience. Use toArx when you want to experiment with the smallest closure bundle and can accept its cache limitations.
Architecture-specific output
Arx and AppImage artifacts are not universal Linux binaries. Build and test one artifact for every architecture you publish, such as x86_64-linux and aarch64-linux.
Two equivalent APIs ​
The portable derivations are available as passthru attributes:
message.toHostScript
message.toArx
message.toAppImageThe same transformations are available as functions:
pkgs.pog.pog.toHostScript message
pkgs.pog.pog.toArx message
pkgs.pog.pog.toAppImage messageThe ordinary message derivation does not fetch or build either bundler. The portable work remains lazy until a transformation is evaluated.
Running single-file outputs through Nix ​
The portable derivations deliberately produce a file at $out, not a conventional package directory containing $out/bin/<program>. That makes nix build create a result symlink directly to the distributable file, but it also means the derivation itself is not a normal nix run target.
Each output therefore exposes a flake app value:
apps.${system}.message-host = message.toHostScript.app;
apps.${system}.message-arx = message.toArx.app;
apps.${system}.message-appimage = message.toAppImage.app;The host-script and Arx app companions execute their files directly. The AppImage app companion uses Nixpkgs' appimage-run, which makes nix run .#message-appimage work on NixOS without relying on a host fusermount3.
The AppImage also exposes the same runner as a conventional derivation:
message.toAppImage.wrappedUnlike an app value, a derivation can be addressed below packages or legacyPackages. This makes nested selectors runnable without adding a top-level app alias:
nix run .#message-appimage.wrapped -- input.json
nix run .#pog.foo.toAppImage.wrapped -- --helpUse .app when defining a consumer flake's apps.<system> output. Use .wrapped when you want a conventional Nix package or a directly runnable nested selector. Both are Nix conveniences and use appimage-run; neither alters the distributable single-file artifact.
Host scripts and dependency discovery ​
toHostScript removes the generated Nix PATH and translates exact executable references into command names. For example:
runtimeInputs = [ pkgs.jq ];
script = ''
${pkgs.jq}/bin/jq -r '.message' "$1"
'';becomes a call to jq. The generated header records the required command and the Nix provider used when creating the script.
Pog automatically adds the main program from every runtimeInputs package. Declare commands invoked by bare name with hostCommands:
hostCommands = [ "curl" "git" ];
script = ''
git status --short
curl --fail --silent --show-error https://example.com/health
'';At startup, the host script checks every required command and reports all missing commands together:
Missing required commands: curl git jqThe generated header lists command names rather than guessing package-manager names:
# Generated by pog. This variant uses programs installed on the host.
# Required commands:
# - bash
# - curl
# - getopt
# - git
# - jqStore data is intentionally rejected ​
Only exact executable references under a dependency's bin directory can be translated safely. A data reference such as:
script = ''
cat ${pkgs.example}/share/example/data.json
'';causes the host-script build to fail. Pog prints the offending line and asks you to use an executable dependency or retain the Nix-backed output. It never emits a supposedly portable script containing hidden /nix/store dependencies.
AppImage on NixOS ​
The raw AppImage supports the usual AppImage launch modes. If FUSE and fusermount3 are available:
./message.AppImage input.jsonUse the extraction fallback when FUSE mounting is unavailable:
APPIMAGE_EXTRACT_AND_RUN=1 ./message.AppImage input.jsonOn NixOS, appimage-run is another supported path:
nix run nixpkgs#appimage-run -- ./message.AppImage input.jsonWhen you export message.toAppImage.app, Pog wires appimage-run automatically:
nix run .#message-appimage -- input.jsonThe wrapped passthru provides the same NixOS launch path without a top-level app export:
nix run .#message-appimage.wrapped -- input.jsonAll AppImage modes still require Linux user namespaces.
Arx behavior and limitations ​
Arx packages the program and its Nix closure into a compressed executable. The launcher extracts the closure into a shared cache under:
$HOME/.cache/tmpx-<hash>The caller's working directory and normal host paths remain visible to the program. The launcher itself still expects these host commands:
sh sed tr date head tar hexdump bzip2Arx also requires Linux user namespaces. Its upstream cache population is not atomic, so simultaneous first executions of the same artifact can race. toArx is therefore experimental.
Copying and publishing artifacts ​
Nix build results are symlinks into the store. Dereference the symlink when preparing a release directory:
mkdir -p dist
nix build .#message-arx --out-link result-arx
nix build .#message-appimage --out-link result-appimage
nix build .#message-host --out-link result-host
cp -L result-arx dist/message-x86_64-linux-arx
cp -L result-appimage dist/message-x86_64-linux.AppImage
cp -L result-host dist/message-host
chmod 755 dist/message-*Use architecture names in published filenames. Before publishing, copy the exact files to a Linux machine or CI runner without Nix installed and exercise the real CLI contract there.
Bundling preserves the license and redistribution obligations of every path in the closure. Review runtimeInputs before distributing an Arx or AppImage, especially when a dependency is unfree or non-redistributable.
CI example ​
Keep fast renderer tests separate from runtime bundle tests. A portable job can build each artifact, run its behavior checks, and then retain the files:
jobs:
portable:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: cachix/install-nix-action@v31
- run: nix run .#test-portable-parity
- run: nix build .#message-arx --out-link result-arx
- run: nix build .#message-appimage --out-link result-appimage
- run: |
printf '{"message":"ci smoke"}\n' > input.json
test "$(./result-arx input.json)" = "ci smoke"
test "$(APPIMAGE_EXTRACT_AND_RUN=1 ./result-appimage input.json)" = "ci smoke"For release confidence, pass the realized files to a second runner that has not installed Nix, assert that /nix is absent, and repeat the smoke tests there.
Troubleshooting ​
Missing required commands ​
The host script found commands absent from PATH. Install every listed command or run the ordinary Nix package instead.
unresolved Nix store references remain ​
The generated script still references store data or another path that cannot be mapped safely to a host command. Use an executable under a dependency's bin directory, redesign that dependency boundary, or keep the Nix-backed output.
Cannot mount AppImage ​
Install FUSE and fusermount3, set APPIMAGE_EXTRACT_AND_RUN=1, or use appimage-run on NixOS.
User namespaces are disabled ​
Arx and AppImage both enter a user-namespace-backed environment. Enable unprivileged user namespaces on the host or use toHostScript instead.
Exec format error ​
The artifact was built for a different CPU architecture. Build it from the matching Nix system and publish architecture-specific filenames.
Current boundaries ​
toHostScriptrequires Bash 4 or newer and GNU-compatiblegetopt.- The host script does not include the ordinary package's separate Bash completion file.
- Bare commands must be declared with
hostCommands. - Arx and AppImage require Linux user namespaces.
- Normal AppImage mounting requires FUSE; extraction and
appimage-runare alternatives. - Arx remains experimental because concurrent first execution can race.
- Portable artifacts still need testing on every architecture and target environment you publish.