Add a post about install sm64ex on the Steam Deck via nix.

This commit is contained in:
Tom Alexander 2025-02-16 10:44:30 -05:00
parent 74ea1c61d8
commit 78365ce1a7
Signed by: talexander
GPG Key ID: D3A179C9A53C0EDE
6 changed files with 650 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

View File

@ -0,0 +1,650 @@
#+OPTIONS: html-postamble:nil
#+title: Install sm64ex on the Steam Deck with Nix
#+date: <2025-02-16 Sun>
#+author: Tom Alexander
#+email:
#+language: en
#+select_tags: export
#+exclude_tags: noexport
[[https://github.com/sm64pc/sm64ex][Sm64ex]] is one of the native ports of Super Mario 64 to the PC. These ports are based on decompiled code from the Nintendo 64 game, enhanced and re-compiled for PC, so they run light and smooth without the overhead and limitations of emulation. To build the final game, you must supply a legally-acquired ROM of Super Mario 64 so that the assets can be extracted and compiled into sm64ex. Therefore a pre-built version cannot be distributed: the end-user must compile sm64ex themselves.
Using Nix, we can easily build our own copy. This has the following advantages:
- Using flake.lock we'll be able to freeze the versions of all build and runtime dependencies, enabling us to rebuild exactly the same version on other Steam Decks, or after wiping our Steam Deck for a clean install.
- All the built and installed packages will be stored under ~/nix~ so it won't alter or interfere with SteamOS and we can easily remove ~nix~ and everything installed through it by deleting ~/nix~.
- We can optionally store our nix config and ~flake.lock~ in ~git~ so we can roll back to any previous version of our nix config and packages.
* Install Nix
"Nix" is the name of:
- A programming language
- A package manager
- A Linux distribution
We are going to be installing Nix the *package manager* on SteamOS, and then writing a config in Nix the *programming language*. While NixOS is by far the best way to use Nix, I want my Deck to stay on SteamOS because I like the idea of having a Valve-managed standardized gaming appliance.
I recommend either setting up ssh access to your deck from another machine, or plugging your steam deck into a dock with a keyboard and mouse attached. This will be deeply tedious with the on-screen keyboard and touch screen.
First, enter desktop-mode and get to a terminal. We are going to be doing a single-user install, so we need to ~chown~ the ~/nix~ directory since it currently belongs to ~root~. If you haven't set a password for the ~deck~ user yet, then set one so we can use ~sudo~:
#+begin_src bash
passwd
#+end_src
Then ~chown~ the ~/nix~ directory:
#+begin_src bash
sudo chown -R deck:deck /nix
#+end_src
Then we need to download the Nix installer script and run it:
#+begin_src bash
wget https://nixos.org/nix/install
# Read the install script and make sure you trust it. Then:
sh install --no-daemon
#+end_src
Now either re-launch your terminal or re-launch your ssh shell to get your environment variables set up. This will:
- Add ~/home/deck/.nix-profile/bin~ to your ~PATH~
- Add ~/home/deck/.nix-profile/share~ and ~/nix/var/nix/profiles/default/share~ to ~XDG_DATA_DIRS~
- Set ~NIX_SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt~
- Set ~NIX_PROFILES=/nix/var/nix/profiles/default /home/deck/.nix-profile~
* Start a Nix config with Flakes and Home-manager
We're going to store our config at src_text[:exports code]{~/.config/mynix}, so we create that directory and then write a few config files to it:
#+begin_src bash
mkdir ~/.config/mynix
#+end_src
And then inside there write a src_text[:exports code]{flake.nix}.
#+begin_src nix
{
description = "My system configuration";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{
nixpkgs,
home-manager,
...
}:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
};
in
{
defaultPackage.${system} = home-manager.defaultPackage.${system};
homeConfigurations."deck" = home-manager.lib.homeManagerConfiguration {
inherit pkgs;
modules = [
{
home.username = "deck";
home.homeDirectory = "/home/deck";
home.stateVersion = "24.11";
programs.home-manager.enable = true;
# Enable flakes
nix = {
package = pkgs.nix;
settings.experimental-features = [
"nix-command"
"flakes"
];
};
# Automatic garbage collection
nix.gc = {
# Runs nix-collect-garbage --delete-older-than 30d
automatic = true;
randomizedDelaySec = "14m";
options = "--delete-older-than 30d";
};
# Deduplicate files in nix store
nix.settings.auto-optimise-store = true;
}
];
};
};
}
#+end_src
This config sets up home-manager with flakes and enables automatic garbage collection to clean up old builds in the nix store.
Then we need to apply the config:
#+begin_src bash
nix --extra-experimental-features 'nix-command flakes' run /home/deck/.config/mynix -- --extra-experimental-features 'nix-command flakes' switch --flake /home/deck/.config/mynix
#+end_src
That's quite the cumbersome command, but after it has been run once it will enable ~nix-command~ and ~flakes~ so the next time we want to apply the config we will only have to run:
#+begin_src bash
nix run /home/deck/.config/mynix -- switch --flake /home/deck/.config/mynix
#+end_src
or
#+begin_src bash
home-manager switch --flake /home/deck/.config/mynix
#+end_src
It may complain about the user systemd session being in a degraded state. That is because these units were in a degraded state before ~Nix~ was even installed. You can ignore this error:
#+begin_src text
The user systemd session is degraded:
UNIT LOAD ACTIVE SUB DESCRIPTION
● app-firewall\x2dapplet@autostart.service loaded failed failed Firewall Applet
● obex.service loaded failed failed Bluetooth OBEX service
#+end_src
The first run of this command generates a file called ~flake.lock~ which stores the revisions for all of our dependencies so if we install this config with this ~flake.lock~ on another machine, it will have exactly the same versions of everything.
* Install sm64ex
We need to update our config to install sm64ex. To do that, first lets tell ~flake.nix~ to load a separate file by adding ~./sm64ex.nix~ to the ~modules~ list.
#+begin_src nix
{
description = "My system configuration";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{
nixpkgs,
home-manager,
...
}:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
};
in
{
defaultPackage.${system} = home-manager.defaultPackage.${system};
homeConfigurations."deck" = home-manager.lib.homeManagerConfiguration {
inherit pkgs;
modules = [
{
home.username = "deck";
home.homeDirectory = "/home/deck";
home.stateVersion = "24.11";
programs.home-manager.enable = true;
# Enable flakes
nix = {
package = pkgs.nix;
settings.experimental-features = [
"nix-command"
"flakes"
];
};
# Automatic garbage collection
nix.gc = {
# Runs nix-collect-garbage --delete-older-than 30d
automatic = true;
randomizedDelaySec = "14m";
options = "--delete-older-than 30d";
};
# Deduplicate files in nix store
nix.settings.auto-optimise-store = true;
}
./sm64ex.nix
];
};
};
}
#+end_src
And we need to create a src_text[:exports code]{~/.config/mynix/sm64ex.nix}:
#+begin_src nix
{
config,
lib,
pkgs,
...
}:
{
imports = [ ];
config = {
home.packages = with pkgs; [
sm64ex
];
# Allow installing sm64ex even though it is marked as non-free
nixpkgs.config.allowUnfreePredicate =
pkg:
builtins.elem (lib.getName pkg) [
"sm64ex"
];
};
}
#+end_src
But we can't apply the config just yet. The sm64ex package [[https://github.com/NixOS/nixpkgs/blob/8bb37161a0488b89830168b81c48aed11569cb93/pkgs/by-name/sm/sm64baserom/package.nix#L10][is expecting that we already have a ~baserom.us.z64~ added to our nix store]]. Legally rip your ~baserom.us.z64~ and add it to your nix store via:
#+begin_src bash
nix-store --add-fixed sha256 baserom.us.z64
#+end_src
Then we apply the config:
#+begin_src bash
home-manager switch --flake /home/deck/.config/mynix
#+end_src
This will compile sm64ex and install it. But there's two problems we can immediately notice:
1. It does not run when launched.
2. There is no convenient way to launch it.
We will be solving these by creating a ~.desktop~ file, introducing nixGL, and writing a wrapper script that makes sm64ex launch from within steam gaming mode properly.
* Adding nixGL
If we were running NixOS, graphical programs would work out-of-the-box. Unfortunately on other Linux Distributions, we need to wrap our graphical programs so they know where to find the graphics drivers. To perform this wrapping, we will use [[https://github.com/nix-community/nixGL][nixGL]]. To install nixGL we need to add it as an input in our ~flake.nix~, apply its overlay, and add it as an ~extraSpecialArgs~ to have it automatically passed into each module:
#+begin_src nix
{
description = "My system configuration";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
nixgl = {
url = "github:nix-community/nixGL";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{
nixpkgs,
home-manager,
nixgl,
...
}:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
overlays = [ nixgl.overlay ];
};
in
{
defaultPackage.${system} = home-manager.defaultPackage.${system};
homeConfigurations."deck" = home-manager.lib.homeManagerConfiguration {
inherit pkgs;
extraSpecialArgs = { inherit nixgl; };
modules = [
{
home.username = "deck";
home.homeDirectory = "/home/deck";
home.stateVersion = "24.11";
programs.home-manager.enable = true;
# Enable flakes
nix = {
package = pkgs.nix;
settings.experimental-features = [
"nix-command"
"flakes"
];
};
# Automatic garbage collection
nix.gc = {
# Runs nix-collect-garbage --delete-older-than 30d
automatic = true;
randomizedDelaySec = "14m";
options = "--delete-older-than 30d";
};
# Deduplicate files in nix store
nix.settings.auto-optimise-store = true;
# Enable the nixGL wrappers
nixGL.packages = nixgl.packages;
}
./sm64ex.nix
];
};
};
}
#+end_src
And then to apply the nixGL wrapper to sm64ex we need to wrap our package install with ~config.lib.nixGL.wrap~ in ~sm64ex.nix~:
#+begin_src nix
{
config,
lib,
pkgs,
...
}:
{
imports = [ ];
config = {
home.packages = with pkgs; [
(config.lib.nixGL.wrap sm64ex)
];
# Allow installing sm64ex even though it is marked as non-free
nixpkgs.config.allowUnfreePredicate =
pkg:
builtins.elem (lib.getName pkg) [
"sm64ex"
];
};
}
#+end_src
Apply the config with:
#+begin_src bash
home-manager switch --flake /home/deck/.config/mynix
#+end_src
and we should be able to launch ~sm64ex~ from the terminal in desktop-mode. Unfortunately, the Steam Deck built-in controls behave oddly in desktop mode, so unless you're using a separate stand-alone controller, you will probably want it set up to run in Steam gaming-mode where the controls behave normally.
* Adding a ~.desktop~ file
The first step to getting sm64ex running in gaming-mode is to create a ~.desktop~ file. ~.desktop~ files are small files with a bit of metadata about a program which can be used by programs such as your "start" / applications menu or an installed program list in Steam.
To add a ~.desktop~ file to the ~sm64ex~ package, we are going to use an "overlay". This overlay will replace the ~sm64ex~ package with a new one we create with the ~buildEnv~ function. This new package will contain the nixGL-wrapped version of the original ~sm64ex~ package and the ~.desktop~ file. We will create the ~.desktop~ file using the ~makeDesktopItem~ function.
First, we need to find a suitable icon. I used a 256x256 PNG from [[https://www.steamgriddb.com/search/icons?term=super+mario+64][SteamGridDB]]. Download whichever icon you want to src_text[:exports code]{~/.config/mynix/icon.png}. Then we can update our ~sm64ex.nix~ to:
#+begin_src nix
{
config,
lib,
pkgs,
...
}:
{
imports = [ ];
config = {
home.packages = with pkgs; [
sm64ex
];
# Allow installing sm64ex even though it is marked as non-free
nixpkgs.config.allowUnfreePredicate =
pkg:
builtins.elem (lib.getName pkg) [
"sm64ex"
];
nixpkgs.overlays = [
(final: prev: {
sm64ex =
let
desktop_item = pkgs.makeDesktopItem {
name = "sm64ex";
desktopName = "Super Mario 64";
comment = "A PC Port of Super Mario 64.";
categories = [
"Game"
];
icon = "sm64ex";
type = "Application";
exec = "sm64ex";
};
in
pkgs.buildEnv {
name = prev.sm64ex.name;
paths = [
(config.lib.nixGL.wrap prev.sm64ex)
];
# We have to use 555 instead of the normal 444 here because the .desktop file ends up inside $HOME on steam deck and desktop files must be either not in $HOME or must be executable, otherwise KDE Plasma refuses to execute them.
postBuild = ''
install -m 555 -D "${desktop_item}/share/applications/"* -t $out/share/applications/
install -m 444 -D "${./icon.png}" $out/share/pixmaps/sm64ex.png
'';
};
})
];
};
}
#+end_src
At this point, reboot to reload the ~.desktop~ files and go back into desktop-mode. You will see ~sm64ex~ in your Start/Applications menu under the "Game" category.
[[./files/start_menu.png]]
* Adding a Steam wrapper
To get controls that work correctly, we still need to add ~sm64ex~ to Steam. Unfortunately, Steam messes with the environment variables, so we need to make a small wrapper script that ensures ~sm64ex~ can find ~libGL.so~. To do this, we will create a package using the ~writeScriptBin~ function and install it with our other packages. Update ~sm64ex.nix~ to:
#+begin_src nix
{
config,
lib,
pkgs,
...
}:
let
steam_sm64ex = pkgs.writeScriptBin "steam_sm64ex" ''
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${pkgs.libglvnd}/lib"
exec ${pkgs.sm64ex}/bin/sm64ex
'';
in
{
imports = [ ];
config = {
home.packages = with pkgs; [
sm64ex
steam_sm64ex
];
# Allow installing sm64ex even though it is marked as non-free
nixpkgs.config.allowUnfreePredicate =
pkg:
builtins.elem (lib.getName pkg) [
"sm64ex"
];
nixpkgs.overlays = [
(final: prev: {
sm64ex =
let
desktop_item = pkgs.makeDesktopItem {
name = "sm64ex";
desktopName = "Super Mario 64";
comment = "A PC Port of Super Mario 64.";
categories = [
"Game"
];
icon = "sm64ex";
type = "Application";
exec = "sm64ex";
};
in
pkgs.buildEnv {
name = prev.sm64ex.name;
paths = [
(config.lib.nixGL.wrap prev.sm64ex)
];
# We have to use 555 instead of the normal 444 here because the .desktop file ends up inside $HOME on steam deck and desktop files must be either not in $HOME or must be executable, otherwise KDE Plasma refuses to execute them.
postBuild = ''
install -m 555 -D "${desktop_item}/share/applications/"* -t $out/share/applications/
install -m 444 -D "${./icon.png}" $out/share/pixmaps/sm64ex.png
'';
};
})
];
};
}
#+end_src
And apply the config with
#+begin_src bash
home-manager switch --flake /home/deck/.config/mynix
#+end_src
* Adding sm64ex to Steam
Finally, we need to tell Steam about ~sm64ex~. Go into desktop-mode and launch Steam inside desktop-mode. Then go to "Add a Non-Steam Game to My Library".
[[./files/add_non_steam_game.png]]
Then find ~sm64ex~ in the list and add it.
[[./files/select_sm64ex.png]]
Then we need to edit the target, so find ~sm64ex~ in your Library, right click on it, and go to Properties.
[[./files/right_click_properties.png]]
And update the target to our new wrapper script to: src_text[:exports code]{"/home/deck/.nix-profile/bin/steam_sm64ex"}.
[[./files/set_target.png]]
Now you should be able to go back into gaming-mode and launch ~sm64ex~ from your Steam library. The controls should work correctly out-of-the-box.
* Final Versions
Now you should be able to go back into gaming-mode and launch ~sm64ex~ from your Steam library. The controls should work correctly out-of-the-box.
Below are the final versions of our config, but you can also browse [[https://code.fizz.buzz/talexander/machine_setup/src/commit/e3a7a410c41cccc28fe235edc802aef7d52c25bc/nix/steam_deck/configuration][my full Steam Deck Nix config]] which includes [[https://github.com/sm64pc/sm64ex][sm64ex (Super Mario 64)]], [[https://github.com/HarbourMasters/Shipwright][Ship of Harkinian (Ocarina of Time)]], and [[https://github.com/HarbourMasters/2ship2harkinian][2Ship2Harkinian (Majora's Mask)]]. It also moves the saves to src_text[:exports code]{"~/.persist"} and loads my configs for each game into place and read-only.
src_text[:exports code]{~/.config/mynix/flake.nix}
#+begin_src nix
{
description = "My system configuration";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
nixgl = {
url = "github:nix-community/nixGL";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{
nixpkgs,
home-manager,
nixgl,
...
}:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
overlays = [ nixgl.overlay ];
};
in
{
defaultPackage.${system} = home-manager.defaultPackage.${system};
homeConfigurations."deck" = home-manager.lib.homeManagerConfiguration {
inherit pkgs;
extraSpecialArgs = { inherit nixgl; };
modules = [
{
home.username = "deck";
home.homeDirectory = "/home/deck";
home.stateVersion = "24.11";
programs.home-manager.enable = true;
# Enable flakes
nix = {
package = pkgs.nix;
settings.experimental-features = [
"nix-command"
"flakes"
];
};
# Automatic garbage collection
nix.gc = {
# Runs nix-collect-garbage --delete-older-than 30d
automatic = true;
randomizedDelaySec = "14m";
options = "--delete-older-than 30d";
};
# Deduplicate files in nix store
nix.settings.auto-optimise-store = true;
# Enable the nixGL wrappers
nixGL.packages = nixgl.packages;
}
./sm64ex.nix
];
};
};
}
#+end_src
src_text[:exports code]{~/.config/mynix/sm64ex.nix}
#+begin_src nix
{
config,
lib,
pkgs,
...
}:
let
steam_sm64ex = pkgs.writeScriptBin "steam_sm64ex" ''
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${pkgs.libglvnd}/lib"
exec ${pkgs.sm64ex}/bin/sm64ex
'';
in
{
imports = [ ];
config = {
home.packages = with pkgs; [
sm64ex
steam_sm64ex
];
# Allow installing sm64ex even though it is marked as non-free
nixpkgs.config.allowUnfreePredicate =
pkg:
builtins.elem (lib.getName pkg) [
"sm64ex"
];
nixpkgs.overlays = [
(final: prev: {
sm64ex =
let
desktop_item = pkgs.makeDesktopItem {
name = "sm64ex";
desktopName = "Super Mario 64";
comment = "A PC Port of Super Mario 64.";
categories = [
"Game"
];
icon = "sm64ex";
type = "Application";
exec = "sm64ex";
};
in
pkgs.buildEnv {
name = prev.sm64ex.name;
paths = [
(config.lib.nixGL.wrap prev.sm64ex)
];
# We have to use 555 instead of the normal 444 here because the .desktop file ends up inside $HOME on steam deck and desktop files must be either not in $HOME or must be executable, otherwise KDE Plasma refuses to execute them.
postBuild = ''
install -m 555 -D "${desktop_item}/share/applications/"* -t $out/share/applications/
install -m 444 -D "${./icon.png}" $out/share/pixmaps/sm64ex.png
'';
};
})
];
};
}
#+end_src