Skip to content
adithyagenie's logo

Using sass-embedded with Nix

Hotpatching JavaScript apps to use Nix-provided Dart Sass

· 4 min

There seem to be many Electron apps which use the sass-embedded npm package. These apps cannot be built (at least directly) with Nix since sass-embedded downloads and uses a pre-built “Dart Sass Embedded” binary dynamically, which will not work reliably inside Nix builds.

Why the issue?#

Nixpkgs provides dart-sass as a package that can be used from the Nix store.

But there is currently no “env” variable that we can use on the code side of things to make the npm package use the dart-sass package from the Nix store.

Hence even by including dart-sass in nativeBuildInputs, we will encounter the error:

error: Cannot build '/nix/store/bly9yqn0cyl6mhdm6v0xxh4djv3d15dg-electron-vite-sass-0.1.0.drv'.
Reason: builder failed with exit code 1.
Output paths:
/nix/store/hphdpa9lzq2c1pbpl1k1czc7pkp1xzb2-electron-vite-sass-0.1.0
Last 25 log lines:
> ✓ built in 23ms
> vite v7.3.2 building client environment for production...
> node:events:487
> throw er; // Unhandled 'error' event
> ^
>
> Error: spawn /build/source/node_modules/.pnpm/sass-embedded-linux-x64@1.99.0/node_modules/sass-embedded-linux-x64/dart-sass/src/dart ENOENT
> at ChildProcess._handle.onexit (node:internal/child_process:287:19)
> at onErrorNT (node:internal/child_process:508:16)
> at process.processTicksAndRejections (node:internal/process/task_queues:90:21)
> Emitted 'error' event on ChildProcess instance at:
> at ChildProcess._handle.onexit (node:internal/child_process:293:12)
> at onErrorNT (node:internal/child_process:508:16)
> at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
> errno: -2,
> code: 'ENOENT',
> syscall: 'spawn /build/source/node_modules/.pnpm/sass-embedded-linux-x64@1.99.0/node_modules/sass-embedded-linux-x64/dart-sass/src/dart',
> path: '/build/source/node_modules/.pnpm/sass-embedded-linux-x64@1.99.0/node_modules/sass-embedded-linux-x64/dart-sass/src/dart',
> spawnargs: [
> '/build/source/node_modules/.pnpm/sass-embedded-linux-x64@1.99.0/node_modules/sass-embedded-linux-x64/dart-sass/src/sass.snapshot',
> '--embedded'
> ]
> }
>
> Node.js v24.15.0
For full logs, run:
nix log /nix/store/bly9yqn0cyl6mhdm6v0xxh4djv3d15dg-electron-vite-sass-0.1.0.drv

using this standard electron-builder (with asar) with pnpm builder in Nix:

{
lib,
stdenv,
fetchPnpmDeps,
pnpmConfigHook,
pnpm,
nodejs_24,
electron_39,
darwin,
copyDesktopItems,
makeDesktopItem,
makeWrapper,
dart-sass,
patchelf,
}:
stdenv.mkDerivation (finalAttrs: rec {
pname = "electron-vite-sass";
version = "0.1.0";
src = lib.cleanSource ./.;
pnpmDeps = fetchPnpmDeps {
inherit (finalAttrs)
pname
version
src
;
fetcherVersion = 3;
hash = "lib.fakeHash";
};
electron = electron_39;
makeCacheWritable = true;
ELECTRON_SKIP_BINARY_DOWNLOAD = 1;
nativeBuildInputs = [
dart-sass # dart-sass has been added as a build input!
nodejs_24
pnpmConfigHook
pnpm
makeWrapper
electron
patchelf
]
++ lib.optionals (stdenv.hostPlatform.isLinux) [ copyDesktopItems ]
++ lib.optionals stdenv.hostPlatform.isDarwin [ darwin.autoSignDarwinBinariesHook ];
postPatch =
""
+ lib.optionalString stdenv.hostPlatform.isLinux ''
# https://github.com/electron/electron/issues/31121
substituteInPlace src/main/index.ts \
--replace-fail "process.resourcesPath" "'$out/share/${pname}/resources'"
'';
buildPhase = ''
runHook preBuild
pnpm --offline electron-vite build
runHook postBuild
'';
postBuild =
lib.optionalString stdenv.hostPlatform.isDarwin ''
# electron-builder appears to build directly on top of Electron.app, by overwriting the files in the bundle.
cp -r ${electron.dist}/Electron.app ./
find ./Electron.app -name 'Info.plist' | xargs -d '\n' chmod +rw
# Disable code signing during build on macOS.
# https://github.com/electron-userland/electron-builder/blob/fa6fc16/docs/code-signing.md#how-to-disable-code-signing-during-the-build-process-on-macos
export CSC_IDENTITY_AUTO_DISCOVERY=false
sed -i "/afterSign/d" package.json
''
+ ''
pnpm --offline electron-builder \
--dir \
-c.electronDist=${if stdenv.hostPlatform.isDarwin then "./" else electron.dist} \
-c.electronVersion=${electron.version} \
-c.npmRebuild=false
'';
installPhase = ''
runHook preInstall
''
+ lib.optionalString stdenv.hostPlatform.isDarwin ''
mkdir -p $out/{Applications,bin}
cp -r dist/**/${pname}.app $out/Applications/
makeWrapper $out/Applications/${pname}.app/Contents/MacOS/${pname} $out/bin/${pname}
''
+ lib.optionalString stdenv.hostPlatform.isLinux ''
mkdir -p $out/share/${pname}
pushd dist/*/
cp -r locales resources{,.pak} $out/share/${pname}
popd
for size in 32 64 128 256 512 1024; do
mkdir -p $out/share/icons/hicolor/"$size"x"$size"/apps
ln -s \
$out/share/${pname}/resources/app.asar.unpacked/resources/assets/icons/"$size"x"$size".png \
$out/share/icons/hicolor/"$size"x"$size"/apps/${pname}.png
done
''
+ ''
runHook postInstall
'';
postFixup = lib.optionalString stdenv.hostPlatform.isLinux ''
makeWrapper ${lib.getExe electron} $out/bin/${pname} \
--add-flags $out/share/${pname}/resources/app.asar \
--add-flags "\''${NIXOS_OZONE_WL:+\''${WAYLAND_DISPLAY:+--ozone-platform-hint=auto --enable-features=WaylandWindowDecorations --enable-wayland-ime=true}}" \
--set-default ELECTRON_FORCE_IS_PACKAGED 1 \
--set-default ELECTRON_IS_DEV 0 \
--inherit-argv0
'';
desktopItems = [
(makeDesktopItem {
name = "${pname}";
desktopName = "${pname}";
comment = "Testing out electron!";
icon = "${pname}";
exec = "${pname} %u";
categories = [
"DocumentViewer"
];
mimeTypes = [ ];
})
];
})

Working around it#

  1. We will monkey-patch the sass-embedded library to accept our dart-sass binary from the Nix store.

  2. Start from a clean git state (stash / commit all your unstaged changes). This is really important since we will be using git diffs to write the new Nix build.

  3. Run pnpm patch sass-embedded to generate the patch folder at node_modules/.pnpm_patches/sass-embedded@1.x.x.

  4. We will patch node_modules/.pnpm_patches/sass-embedded@1.x.x/dist/lib/src/compiler-path.js to include the lines:

const binPath = process.env.SASS_EMBEDDED_BIN_PATH;
if (binPath) {
return [binPath];
}
  1. The file should look something like this:
const p = require("path");
const compiler_module_1 = require("./compiler-module");
/** The full command for the embedded compiler executable. */
exports.compilerCommand = (() => {
// We inject the binary via the SASS_EMBEDDED_BIN_PATH env.
// Add these here:
const binPath = process.env.SASS_EMBEDDED_BIN_PATH;
if (binPath) {
return [binPath];
}
// The rest of the code...
try {
return [
require.resolve(`${compiler_module_1.compilerModule}/dart-sass/src/dart` +
(process.platform === 'win32' ? '.exe' : '')),
require.resolve(`${compiler_module_1.compilerModule}/dart-sass/src/sass.snapshot`),
];
  1. Then run pnpm patch-commit 'node_modules/.pnpm_patches/sass-embedded@1.x.x' to generate patches/sass-embedded.patch file containing a diff of the changes you just did.

With this patch, the sass-embedded package will accept the SASS_EMBEDDED_BIN_PATH env var for the binary.

You should now see unstaged changes across three files: package.json, pnpm-lock.yaml and the newly generated patch file.

Now, generate a diff file of all these three files by:

  1. git add package.json pnpm-lock.yaml patches/sass-embedded.patch
  2. git diff --staged > 0001-build-enable-specifying-custom-sass-compiler-path-by.patch

Now, keep the patch file you just generated and reset all the other changes with git reset --hard HEAD. Your only unstaged/untracked file must be 0001-build-enable-specifying-custom-sass-compiler-path-by.patch.

  1. Modify your Nix build script to add the patch file and set the env var:
# ...
pnpmDeps = fetchPnpmDeps {
inherit (finalAttrs)
pname
version
src
;
fetcherVersion = 3;
hash = lib.fakeHash;
};
# Apply the patch
patches = [ ./0001-build-enable-specifying-custom-sass-compiler-path-by.patch ];
# add dart-sass
nativeBuildInputs = [
dart-sass
# the rest of the build inputs...
];
# other build steps...
buildPhase = ''
runHook preBuild
export SASS_EMBEDDED_BIN_PATH="${dart-sass}/bin/sass"
pnpm --offline electron-vite build
runHook postBuild
'';
# the rest remains the same

Your build should now succeed without any errors (relating to sass-embedded at the least).

For reference, the final build script and patch should look something like this.

Further maintenance and updates#

  • The patch file must be updated through the same procedure if you decide to bump the version of sass-embedded package.