Skip to content
waltmckPublic

About

Declarative proprietary apps, packaged with Nix

Resources

Stars

6 stars

Watchers

0 watching

Forks

Latest commit

 

History

70 Commits

Folders and files

Repository files navigation

propnix

Reproducible Nix packaging of proprietary games/apps to run on Linux. The goal is to provide sensible defaults that make apps run well (i.e. constructing a WINE prefix, overriding certain DLLs, choosing an emulator/emulator options), abstracting the details of emulation to provide a consistent experience between platforms.

Supported fetchers are GOG and Steam. You will need to initialize your credentials (see Credentials). Quick start:

nix run .#propnix -- cred add steam   # And follow the instructions to login
nix run .#factorio --extra-sandbox-paths /propnix=/var/lib/propnix

This is fully reproducible (every download is pinned in the game's versions.json). The derivation will download the game as a FOD using your credentials, then wrap it to look like a native linux application.

You can globally configure which fetchers to download from by using

with propnix.lib.mkScope { inherit pkgs; config.preferredFetchers = /* subset of */ [ "steam" "gog" ]; };

Alternatively, you can override the fetcher used on a per-game basis depending on your ownership like

factorio.apply { fetcher = "gog" }

This apply idiom (inspired by wrappers) is a common general pattern: almost every knob for propnix packages is tunable via a module system exposed with apply.

Configuration in DOCS.md.

Supported games

Core to Nix is the [concept]](https://ryantm.github.io/nixpkgs/stdenv/cross-compilation/#sec-cross-packaging) of a buildPlatform, hostPlatform, and targetPlatform. The last can be safely ignored here; the first two are attributes of a derivation that describe respectively the architecture needed to build the package and the architecture needed to run the package. Although propnix provides a cache for the compiled portions of the project (e.g. our patched WINE and FEX), in order to support users who want to build from source we will always make buildPlatform equal hostPlatform.

This project is somewhat unusual for Nix because it packages binaries built for a different architecture alongside their emulation stacks. In our case, a different abstraction is necessary: what is the architecture that the proprietary binary was built for (and that we need to emulate)? We call this emulatedPlatform. Often a single game may be available on multiple emulatedPlatforms, so the purview of this project is to support running several of these and experimentally find the best one (balancing performance and compatibility).

Often different emulatedPlatforms will be available on different fetchers (for example, Factorio for aarch64-linux is available on Steam but not on GoG). If the emulatedPlatform is overridden with apply, the module system will try to pick a fetcher to satisfy that preference (and it will fail with an error if it is unable to).

How to read these tables. The below table give a matrix of supported fetchers and emulatedPlatforms on different hosts. Bold marks the default chosen by the project: propnix picks it from the game's own quality ranking, filtered by what your host can run.

As usual, it is possible to pick the emulatedPlatform yourself using apply. An example:

nix run --impure --expr '(builtins.getFlake (toString ./.)).legacyPackages.${builtins.currentSystem}.hollow-knight.apply { emulatedPlatform = "x86_64-windows"; }'
nix run --impure --expr '(builtins.getFlake (toString ./.)).legacyPackages.${builtins.currentSystem}.hollow-knight.apply { fetcher = "steam"; emulatedPlatform = "x86_64-linux"; }'

Table 1 (by host): which builds run on my machine?

Rows are games, columns are the architecture you are running on, and cells are the emulatedPlatform values that work there. The bolded emulatedPlatform is the default one.

game on an aarch64-linux host on an x86_64-linux host
baba-is-you x86_64-linux x86_64-linux
baby-steps x86_64-windows x86_64-windows
baldurs-gate-3 x86_64-windows x86_64-windows
casualties-unknown-demo x86_64-windows x86_64-windows
cities-skylines x86_64-windows x86_64-windows
civilization-5 i386-linux — (needs a 32-bit GL stack)
civilization-6 x86_64-windows x86_64-windows
cyberpunk-2077 x86_64-windows x86_64-windows
dont-starve i386-windows i386-windows
factorio aarch64-linux, x86_64-linux, x86_64-windows x86_64-linux, x86_64-windows
fallout-nv i386-windows i386-windows
hollow-knight x86_64-linux, x86_64-windows x86_64-linux, x86_64-windows
hollow-knight-silksong x86_64-windows x86_64-windows
homeworld-rm i386-windows i386-windows
iron-lung x86_64-windows x86_64-windows
iron-nest x86_64-windows x86_64-windows
kerbal-space-program x86_64-windows x86_64-windows
no-mans-sky x86_64-windows x86_64-windows
outlast x86_64-windows x86_64-windows
outlast-2 x86_64-windows x86_64-windows
papers-please x86_64-windows x86_64-windows
potionomics x86_64-windows x86_64-windows
prison-architect x86_64-windows x86_64-windows
repo x86_64-windows x86_64-windows
rust x86_64-windows x86_64-windows
shadow-of-mordor x86_64-windows x86_64-windows
skyrim-se x86_64-windows x86_64-windows
space-engineers x86_64-windows x86_64-windows
stellaris x86_64-linux x86_64-linux
victoria-3 x86_64-windows x86_64-windows
witcher-3 x86_64-windows x86_64-windows

Table 2 (by store): which fetcher provides a given game build?

Rows are games, columns are stores. A cell lists the emulatedPlatform values propnix has pinned from that store, independent of any host. This is the table to read when deciding which account you need: a game with entries under only one column can only be built by someone who owns it there.

game gog steam
baba-is-you — x86_64-linux
baby-steps x86_64-windows —
baldurs-gate-3 x86_64-windows x86_64-windows
casualties-unknown-demo — x86_64-windows
cities-skylines — x86_64-windows
civilization-5 — i386-linux
civilization-6 — x86_64-windows
cyberpunk-2077 x86_64-windows —
dont-starve i386-windows —
factorio x86_64-windows aarch64-linux, x86_64-linux, x86_64-windows
fallout-nv i386-windows —
hollow-knight x86_64-windows x86_64-linux, x86_64-windows
hollow-knight-silksong x86_64-windows —
homeworld-rm i386-windows —
iron-lung x86_64-windows —
iron-nest x86_64-windows —
kerbal-space-program x86_64-windows —
no-mans-sky x86_64-windows —
outlast x86_64-windows —
outlast-2 x86_64-windows —
papers-please x86_64-windows —
potionomics — x86_64-windows
prison-architect x86_64-windows —
repo — x86_64-windows
rust — x86_64-windows
shadow-of-mordor x86_64-windows x86_64-windows
skyrim-se x86_64-windows x86_64-windows
space-engineers — x86_64-windows
stellaris — x86_64-linux
victoria-3 — x86_64-windows
witcher-3 x86_64-windows —

When a game is pinned from both stores, the default fetcher follows your preferredFetchers config, so a gog-only setup automatically falls back to the GOG build of a game whose Steam build would otherwise win (provided the game's own ranking sanctions that platform).

Requirements

propnix assembles a throwaway WINEPREFIX per launch out of kernel bind/overlay mounts in a private user+mount namespace, so it does not need any privileges while still maintaining the performance/cache benefits of native kernel mounts. That design leans on several host capabilities.

At a glance (full detail — plus the repo layout, how to add a game, and the backlog — is in DOCS.md):

requirement needed for required?
Unprivileged user namespaces (CONFIG_USER_NS, user.max_user_namespaces > 0) assembling the prefix (propnix-mount) yes
Unprivileged network namespaces (CONFIG_NET_NS) the kernel-enforced offline guarantee for a game that declares online = false — the launcher adds CLONE_NEWNET to the same unshare as the userns, so the game gets loopback and nothing else yes for those games (most of them)
Unprivileged overlayfs + userxattr in a userns (Linux 5.11+) copy-on-write prefix/game/save overlays over the store yes
user.* xattrs + overlay-upper support on $XDG_STATE_HOME & saves fs (ext4/xfs/btrfs/tmpfs; ZFS ≥ 2.2) persisting the overlay uppers yes
A Vulkan ICD (e.g. Mesa on Asahi) the default DXVK/vkd3d D3D backend hard unless PROPNIX_WINE_D3D=wined3d
Wayland (+ Xwayland) the GTK4 splash + the game window yes
/dev/ntsync (Linux 6.14+) fast wine synchronization recommended
A toplevel-list protocol — ext-foreign-toplevel-list-v1 (Mutter 45+, wlroots), wlr-foreign-toplevel-management, or org_kde_plasma_window_management (KDE) — or an X11/Xwayland session single-instance raise, splash dismiss, close-to-quit optional (degrades gracefully; every desktop provides at least one, and the only gap is raising a native-Wayland window on GNOME)
GOG/Steam account owning the title + its token building a game payload (FOD fetch) yes (build-time)
Nix's classic build users, i.e. auto-allocate-uids = false reading that token inside the FOD sandbox — an auto-allocated build runs as a synthetic uid/gid in no host group, so a 0640 token is unreadable and only a world-readable one would work yes (build-time)

How it runs

mkApp evaluates a game's module (two orthogonal axes: fetcher × emulatedPlatform) and dispatches to the selected backend's builder (mkWineApp / mkThinApp), producing bin/<name> — a wrapper around propnix-launcher with a baked JSON config (store paths + defaults + the seal + the mount table). The scope injects the arch-appropriate emulator set, but the launcher, config, and game spec are identical. For Windows content that is wine — on aarch64 with FEX + ARM64EC DXVK/vkd3d, on x86_64 native with standard DXVK/vkd3d. A Linux build needs no prefix at all, and on aarch64 gets whichever emulator its ABI requires: box64 for an x86_64 payload, FEX for a 32-bit x86 one (box64 emulates x86_64 only), and nothing at all for an ARM64 payload. An x86_64 host runs every x86 Linux build directly. At launch the launcher:

  1. single-instance — an flock keyed on the appid; a duplicate launch focuses the running window and exits. The raise tries three paths in order, matching the game's app_id — or, while the game is still cold-starting, this game's startup splash: EWMH _NET_ACTIVE_WINDOW, which covers an Xwayland game window under any compositor; then wlr-foreign-toplevel-management (the wlroots family — Hyprland/sway/Wayfire/river — plus COSMIC); then KDE's org_kde_plasma_window_management, since KWin implements neither of the first two and this is its only activate verb (it needs the X-KDE-Wayland-Interfaces grant that every game's .desktop file carries). The single gap is a native-Wayland window on GNOME, which exposes no activate verb to foreign clients at all — there it degrades to a no-op.
  2. prefix — assembles the WINEPREFIX from a declarative mount table (there is no symlink farm and no seeding step): propnix-mount unshares a private user+mount namespace and lays a fresh tmpfs at the view root, then binds the read-only system tree (wine-prefix-lower: C:\Windows, Program Files, dosdevices, the HKLM/.Default hives) and stacks CoW overlays over the store for the writable parts (the user profile, ProgramData, the HKCU hive) so writes persist to $XDG_STATE_HOME without any copy. On the DXVK backend, the native ARM64EC DXVK/vkd3d DLLs are bound over the matching system32 builtins.
  3. saves — the declared save location is a mount row binding a host dir out of the prefix (created on first launch, or the launch refuses if you pointed PROPNIX_SAVE_DIR somewhere missing).
  4. seal — a targeted env scrub (unset only WINE*/FEX_*/BOX64_*/LD_*, keep the rest of the session env) then sets the meant vars. Never env_clear().
  5. run — spawns wine in its own process group; tears down prefix-scoped (wineserver -k) on exit — never a global process-name kill.

PROPNIX_* — global runtime knobs

Every default the config bakes is overridable by an env var, so a user or NixOS module can set one once to steer all games:

var effect
PROPNIX_WINE_D3D dxvk (default) or wined3d
PROPNIX_WINE_GRAPHICS wayland (default for HK) or x11
PROPNIX_FPS frame cap (any positive int) → DXVK_FRAME_RATE, and forces vsync off (dxgi.syncInterval=0) so the cap paces via timer instead of FIFO vblank quantization. DXVK/vkd3d only (no-op on wined3d). Unset → the game's own vsync/cap are left untouched.
PROPNIX_BENCH on-screen MangoHud overlay on the Vulkan backend (DXVK + vkd3d) — fps/frametime/CPU/versions (GPU stats need a MangoHud-supported driver; blank on Asahi). On wined3d there is no overlay — fps prints to the console via wine's +fps channel. Also tees the game's output to the console.
PROPNIX_DEBUG tee the game's merged stdout+stderr to the launcher's stdout (no bench overlay) — for troubleshooting a launch
PROPNIX_SAVE_DIR global saves root, namespaced per app ($PROPNIX_SAVE_DIR/<appid>). Set-but-missing is a hard error; unset → $XDG_DATA_HOME/propnix-saves.
PROPNIX_WINEDEBUG override WINEDEBUG (default -all)
PROPNIX_NO_PREFETCH skip the cold-launch prefetch (the wine path warms the assembled prefix's PE modules — .dll/.drv/.exe, system32 + the game's own — into the page cache while wine starts)
PROPNIX_EXTRA_BINDS extra ;-separated TARGET|SOURCE bind rows (TARGET prefix-relative or $-expandable; SOURCE an absolute/$-expandable host path) — an ad-hoc redirect (e.g. a secondary saves/mods dir) without a rebuild
PROPNIX_STEAM_ACCOUNT which stored Steam account propnix pin / propnix hash use when several are stored (flag beats env beats try-all). Not read at launch time — the gbe_fork offline-entitlement shim manages its own identity.

Debug escape hatches: --shell (sealed shell in the prefix), --propnix-unseal (skip the scrub).

Credentials

The credentialed fetchers need an account token, kept out of the store. Add an account with the propnix CLI (gog for GOG payloads, steam for Steam depots):

propnix cred add gog             # opens a browser login; paste back the redirect URL
propnix cred add steam           # DepotDownloader's Steam Guard 2FA login (one-time)
propnix cred list                # accounts, grouped by type, labelled by username
propnix cred rm <username>       # or, if a username exists under >1 type:
propnix cred rm --type steam <username>

cred add gog opens GoG's login in your browser (GoG blocks headless password login — captcha/2FA happen there); you paste the resulting …/on_login_success?code=… URL back, and it mints + stores the token. cred add steam drives DepotDownloader through Steam's one-time Steam Guard 2FA and stores the reusable refresh token. Both populate the store at /var/lib/propnix — credentials.toml (a non-secret pointer) plus <type>/<username>/<tokenfile> (gog/…/galaxy_tokens.json, steam/…/depotdownloader-store.tar), mode 0640 and group-owned by propnix-fetch so the build sandbox can read it. On NixOS the whole flow is unprivileged — the store dirs are group-writable by propnix, so no step prompts for a password. Without the module (or with a group it names absent) the CLI falls back to the historical layout: dirs 0755, group nixbld, and the /var/lib write via sudo — the login itself always runs as you. Multiple accounts are supported everywhere: a fetcher tries each of its type until one owns the pinned content, and so does propnix pin — it advances to the next stored account on an ownership refusal and settles the question before a byte of content is fetched. Force one account with --gog-account <name> / --steam-account <name>, or with PROPNIX_GOG_ACCOUNT / PROPNIX_STEAM_ACCOUNT (flag beats env beats try-all). cred rm takes --type to disambiguate when the same username is stored under two backends.

Bind the store into the build sandbox with the NixOS module (services.propnix.enable) or manually (--extra-sandbox-paths /propnix=/var/lib/propnix, requires a trusted Nix user). The store holds only the pointer + tokens; every download parameter is pinned in versions.json, so the bind can only make a fetch succeed or fail, never change what is fetched. See config/credentials.nix for the full model.

Prefer keeping the token in your (encrypted) config repo? The module also builds the store declaratively from any secret manager that decrypts to a path — sops-nix, agenix, a hand-rolled unit:

sops.secrets."propnix/gog/alice" = {
  sopsFile = ./secrets/gog-alice.json;                # the encrypted galaxy_tokens.json
  format = "binary";                                  # keep the file verbatim
  restartUnits = [ "propnix-credentials.service" ];   # re-copy when you rotate it
};
services.propnix.credentials.gog.alice.source = config.sops.secrets."propnix/gog/alice".path;

propnix-credentials.service copies each declared token into the same <type>/<username>/<tokenfile> layout at activation (it copies rather than symlinks because sops-nix's path points into /run/secrets.d/…, which doesn't exist inside the build sandbox). Declared and cred add-ed accounts coexist; set services.propnix.credentialsPath = "/run/propnix" for a fully declarative host and no token ever touches the disk. A declared credential stays the config's: cred list marks it (declarative) and cred rm refuses it, naming the option to edit.

$ propnix cred list
GOG:
  - alice (declarative)
  - dave
Steam:
  - bob (declarative)

Either way, managing credentials means being in the propnix group the module creates. Its members default to nix.settings.allowed-users — i.e. *, every human account, unless you've narrowed who may use the daemon — so cred add, cred rm and propnix pin all work without sudo out of the box. Restrict or extend it with:

services.propnix.allowedUsers = [ "you" ];     # or [ "@wheel" ], or [ ] to opt out entirely
users.users.you.extraGroups = [ "propnix" ];   # equivalent, honoured alongside the option

There are two groups, because read and write have to be separable: propnix owns the store directories (2775, so members write without sudo), while a derived propnix-fetch holds those same humans plus the Nix build users and owns the token files (0640). That way a build can read a credential — nix passes a build user's supplementary groups to the builder, which is what keeps the sandboxed fetcher working — but never write one. You only ever touch propnix; propnix-fetch is kept in sync for you. Adding yourself to nixbld instead would be a mistake — its members are exactly who nix runs builds as.

About

Declarative proprietary apps, packaged with Nix

Resources

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages