Electron apps were historically hard to package in Nix (see this cool meme), but apparently it is not true anymore.
How can I package an electron application in nix, and quickly develop npm-related things in NixOs?
Electron apps were historically hard to package in Nix (see this cool meme), but apparently it is not true anymore.
How can I package an electron application in nix, and quickly develop npm-related things in NixOs?
Share Improve this question edited Feb 16, 2024 at 2:39 tobiasBora asked Feb 16, 2024 at 1:32 tobiasBoratobiasBora 2,53622 silver badges34 bronze badges1 Answer
Reset to default 9(disclamer: I am not a JS expert at all, just trying to wrap my head around electron packaging)
TL;DR: If you only want to package, and already have an existing project, you can jump straight to the section "do the packaging part" and copy/paste the .nix
file (quite simple, nothing too magic). Yet, I explain before how to develop efficiently in NixOs etc. I also made a minimal project here that you can test with:
$ nix run github:tobiasBora/basic-nix-packaging
There is nothing more to do when using electron-forge or an external library, yet I did some demo here:
$ nix run github:tobiasBora/basic-nix-packaging/electron-forge
$ nix run github:tobiasBora/basic-nix-packaging/using-library
How to quickly develop on NixOs
Npm/electron has a culture of often packing pre-built binaries (which is not great, for instance in term of security but it is another question), that expects a loader in, e.g., /lib64/ld-linux-x86-64.so.2
. This can be a problem if you are on NixOs as NixOs removes by default the loaders for maximum reproducibility (see more details and solution here). What I remend if you do not want to bother anymore and just follow the usual tutorials usable on any standard linux distribution, is to enable in your configuration.nix
:
programs.nix-ld.enable = true;
## If needed, you can add missing libraries here. nix-index-database is your friend to
## find the name of the package from the error message, like:
## $ nix run github:mic92/nix-index-database missinglib.so
## More details: https://github./nix-munity/nix-index-database, you might like
programs.nix-ld.libraries = options.programs.nix-ld.libraries.default ++ (with pkgs; [
# put here missing libraries
]);
(you might need to reboot to propagate correctly the environment variables)
With that, you can use npm
like in any other system!
Now, you might want to package your programs in a clean and pure way, that you can install with your favorite flake etc… Let's see how to do it now.
Create an electron project (nothing interesting here)
For this tutorial, we will create a basic electron project as explained in the tutorial (as I have nix-ld
enabled):
$ mkdir my-electron-app && cd my-electron-app
$ nix-shell -p nodejs_latest
$ npm init # do set an author & description, and entrypoint = main.js
$ npm install --save-dev electron
then add in the script
section of package.json
a start
mand like in:
{
"scripts": {
"start": "electron ."
}
}
Then, create the files listed in https://www.electronjs/docs/latest/tutorial/quick-start, notably:
main.html
:
// main.js
// Modules to control application life and create native browser window
const { app, BrowserWindow } = require('electron')
const path = require('node:path')
const createWindow = () => {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.webContents.openDevTools();
// and load the index.html of the app.
mainWindow.loadFile('index.html')
// Open the DevTools.
// mainWindow.webContents.openDevTools()
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
// On macOS it's mon to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// Quit when all windows are closed, except on macOS. There, it's mon
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
index.html
:
<!--index.html-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
We are using Node.js <span id="node-version"></span>,
Chromium <span id="chrome-version"></span>,
and Electron <span id="electron-version"></span>.
</body>
</html>
and preload.js
:
// preload.js
// All the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}
for (const dependency of ['chrome', 'node', 'electron']) {
replaceText(`${dependency}-version`, process.versions[dependency])
/* replaceText(`${dependency}-version`, process.versions[dependency]) */
}
})
Run the project (optional)
If you only want to package without testing "interactively" your software, you can skip this section.
Method 1: If you do not have nix-ld
installed, or want to maximize purity, you might want here to use electron
packaged from NixOs. Just enter into a shell with:
$ nix-shell -p electron
$ electron .
and it will start your project. Later, once we create a default.nix
, you can just run:
$ nix-shell
$ electron .
and it will automatically install electron (it also works if you are using external libraries)
Method 2: On the other hand, if you run:
$ npm start
it will try to run the npm-installed electron. If you have nix-ld
installed with all the libraries, it should work. If you get an error like:
XXX: no such file or directory
or an error saying that you cannot run binaries on NixOs, then it likely means that you run NixOs and don't have nix-ld
enabled (cf section `How to quickly develop on NixOs, make sure to reboot after that). If you get instead:
/tmp/electrontest/my-electron-app/node_modules/electron/dist/electron: error while loading shared libraries: libdrm.so.2: cannot open shared object file: No such file or directory
then it means that you need to install in nix-ld
the package providing libdrm.so.2
:
$ nix run github:mic92/nix-index-database -- libdrm.so.2 --top-level
xorg_sys_opengl.out 0 s /nix/store/br48s8nkd9d2y2qxzd52v9rsqhh5zrl1-xorg-sys-opengl-3/lib/libdrm.so.2
xorg_sys_opengl.out 0 s /nix/store/br48s8nkd9d2y2qxzd52v9rsqhh5zrl1-xorg-sys-opengl-3/lib/libdrm.so.2.4.0
libdrm.out 0 s /nix/store/lmqz8wx07avf4c5d0qqf0h5hwjni9yrj-libdrm-2.4.120/lib/libdrm.so.2
libdrm.out 110,352 x /nix/store/lmqz8wx07avf4c5d0qqf0h5hwjni9yrj-libdrm-2.4.120/lib/libdrm.so.2.4.0
Here, you see that you want the library libdrm
. Just add it to programs.nix-ld.enable
as shown above, and keep going with other libraries (in my test I needed no reboot in KDE Plasma, but my setting is a bit different (older version), but if you see that your changes are not taken into account, you might need a reboot). It might be a bit annoying the first time, but once your list is long enough, it should work for most programs (maybe we could propose a more plete set of options in nixpkgs). In my case, I notably needed to add:
libdrm
mesa
libxkbmon
(but I already made a long enough list that I describe here)
Do the packaging (interesting part)
To package this, just create this file called, say, package.nix
:
# Inspired by pkgs/applications/editors/uivonim/default.nix
# and pkgs/by-name/in/indiepass-desktop/package.nix
{ lib, buildNpmPackage, fetchFromGitHub, electron }:
buildNpmPackage rec {
pname = "my-electron-app";
version = "0.1";
src = ./.;
npmDepsHash = ""; # you will get an error about mismatching hash the first time. Just copy the hash here
# Useful for debugging, just run "nix-shell" and then "electron ."
nativeBuildInputs = [
electron
];
# Otherwise it will try to run a build phase (via npm build) that we don't have or need, with an error:
# Missing script: "build"
# This method is used in pkgs/by-name/in/indiepass-desktop/package.nix
dontNpmBuild = true;
# Needed, otherwise you will get an error:
# RequestError: getaddrinfo EAI_AGAIN github.
env = {
ELECTRON_SKIP_BINARY_DOWNLOAD = 1;
};
# The node_modules/XXX is such that XXX is the "name" in package.json
# The path might differ, for instance in electron-forge you need build/main/main.js
postInstall = ''
makeWrapper ${electron}/bin/electron $out/bin/${pname} \
--add-flags $out/lib/node_modules/${pname}/main.js
'';
}
and a file default.nix
containing:
{ pkgs ? import <nixpkgs> {} }:
pkgs.callPackage ./package.nix {}
Build and run with:
$ nix-build
$ ./result/bin/my-electron-app
(you will need to update the hash in the above code, as you will get an error the first time with the valid hash)
Enjoy!
You can also type:
$ nix-shell
and you will get a shell with electron installed so that you can debug with:
$ electron .
Alternative: using electron-forge
electron-forge is the officially remended solution to package electron application. You can prepare your project as follows:
$ npm install --save-dev @electron-forge/cli
$ npx electron-forge import
In theory, if you want to package a .deb
or .rpm
and have nix-ld
enabled, you should be able to do:
$ nix-shell -p dpkg fakeroot rpm
$ npm run make
but I don't know why it plains with an error: file "../../../../../../../nix/store/pryizz83lb9hvjknaqyl5f54d5bai3xd-my-electron-app-0.1" links out of the package
.
Nevermind, the goal is to package it for nix now. And good luck: you have nothing more to do, the above script also works for this one (let me know if for more plex cases it fails).
You can try this version also on the same repository on the electron-forge
branch:
$ nix run github:tobiasBora/basic-nix-packaging/electron-forge
Yarn
If you use yarn, you can also use mkYarnPackage
, I've not tried but I expect this to be similar (even if I heard that yarn was not as nice as npm to package in nix).
Further readings
Here are some other related tutorials I wrote:
- More details on how to create and test your derivation https://unix.stackexchange./questions/717168/how-to-package-my-software-in-nix-or-write-my-own-package-derivation-for-nixpkgs
Other related resources:
- https://github./NixOS/nixpkgs/issues/46382
- In nixpkgs, install ripgrep, and run
rg "buildNpmPackage" $(rg "electron" -l)
and you will see all files that contain bothbuildNpmPackage
andelectron
: useful to get some examples. If you only want to read the simpler packages, you can see the length of the files usingfor x in $(rg "buildNpmPackage" $(rg "electron" -l) -l); do echo "$x"; cat "$x" | wc -l; done
. You also have a few examples using yarn (usefor x in $(rg "mkYarnPackage" $(rg "electron" -l) -l); do echo "$x"; cat "$x" | wc -l; done
)