Making a development flake for Bespoke Synth

Lately I’ve been wanting to start developing bespoke synth. To begin with, I’ve made a development flake for it (flake code is here). From what I can tell, everything on the flake side of things is working properly. However, the builds produced by the environment have a small problem, everything is rendered a few pixels above where it is supposed to be rendered.

Normal draw:
good bespoke

Offset draw:
bad bespoke

I copied the inputs for the devshell from the bespokesynth package in nixpkgs, so I don’t think it’s a dependency problem. To build it, I’m just running the build command, as defined in the flake, which was initially taken from the build section of the readme. Does anyone know how I might fix this? I would love to submit this flake as a PR to bespoke so other people can benefit from it.

Beyond the immediate problem, I have a few questions regarding the usefulness of my flake. Would someone on Windows be able to use the flake to build and develop for Windows? Would it be hard to make it possible to cross-compile the application for Windows or MacOS? How can I imporove the scripts I’m making with writeScriptBin? (Are they useful? Is there a better way to structure them? Can repo-path be removed cleanly?) What else can I do to make my flake better or extend it? I want to make it as useful as possible for anyone who might want to develop bespoke.

One other thing I’d like to ask that relates to NixOS in general is, do I have to make a flake for everything I want to develop? I see that there’s the nix develop command, but that doesn’t use the shell I like (zsh), so I tend to stay away from it. And unfortunately, nix-shell -p doesn’t seem to work for setting up a development environment.

Thank you for reading!

cc: @tobiasBora

A very similar issue was reported here Menu unusable on Pantheon DE · Issue #680 · BespokeSynth/BespokeSynth · GitHub, it was caused from missing X11 related libraries that are dlopenned by bespokesynth, mkShell does not populate these variable since they are not needed at build time. One solution might be to add a shell hook in mkshell that setup LD_LIBRARY_PATH appropriately, but this does not seem like a really clean solution. Instead, I would say that it is better to change the way bespoke is compiled, by adding a flag (see this discussion for, e.g., advices on how to debug it) that adds in the binary’s a rpath entry to help dlopen to find the file:

NIX_LDFLAGS = "-rpath ${lib.makeLibraryPath (with xorg; [ libX11 libXrandr libXinerama libXext libXcursor libXScrnSaver ])} $NIX_LDFLAGS";
dontPatchELF = true; # needed or nix will try to optimize the binary by removing "useless" rpath

(note that I’ve not tested the above code, but it should be close enough to the solution)

Regarding your build command, it might be enough (not sure if cmake needs additional hook to configure the environment… guess it’s fine if you can run the command), but note also that you can run the default phase (./configure…) using exacly the command used by nix by using phases="phasesYou wantToRun" genericBuild as described in more details here Nixpkgs/Create and debug packages - NixOS Wiki

Regarding the writeScriptBin, I have to admit that I find that nix could provide some more user friendly way to automatically compile a project, but I’m not aware of a better strategy except for the generic way to run a phase described above.

Finally, regarding flake, of course you don’t need flake (you can use plain nix if you prefer for instance), but flake provides nice tools to help with purity, caching… Regarding nix develop and zsh, one simple solution to use zsh inside nix develop is to simply manually run zsh inside nix develop, it should work directly. You can also type:

$ nix develop -c zsh

as discussed here, possibly defining an alias if you use it a lot. There is also direnv that is handy to automatically enter into the shell when changing folders…

1 Like

Thanks for your response! dlopen() strikes again!

I see what you mean, nix develop provides them as variables that contain the entire script, which is pretty cool! I’m not sure how I should go about providing functions like this in the flake. If there’s a way to get them from nixpkgs I might do that, but I’m not sure how or even if I should do that. I would think the flake should be as self-contained as possible so it can be modified easily and it’s clear what’s going on.

I’m a little confused how I should apply this. Since bespoke takes a while to build from scratch (long enough to make it hard to quickly iterate on a problem), I’m trying to take advantage of build artifacts as much as I can, and I’m not sure if nix is able to use them (with nix build). I saw when I compiled it through nix develop and ran the phases by hand, I was able to use the build artifacts, which was great! I would want to have a way to make it output a debug build rather than a release build if it’s for development. I assume I can add overrides to the package via overrideAttrs somewhere and use the modified version in the flake. Unfortunately, using nix develop and running the phases manually doesn’t seem to make any impact on the offset draw problem. Based on what you’re saying the problem is though, I wouldn’t expect it to solve it.

In an earlier (jankier) iteration of this flake, I took all the inputs from the bespoke synth package using mkShell.inputsFrom, and I implemented the apps attribute by overriding the source to point to the current directory. This actually worked pretty well, but it built everything any time you wanted to compile a new build (no build artifacts), and it required that the files you’re using be tracked by git in the local repo so nix could get them when it builds. Not too optimal. At least it didn’t have the offset problem I’ve made :stuck_out_tongue:

Another thing I would like to explore at some point is adding the apps attribute to the flake outputs for compiling clean release builds. Maybe then I’d be able to more easily use something like this?

Unfortunately, using -c zsh doesn’t give the shell all the build phase variables ($buildPhase, $configurePhase, etc), so it’s not quite as useful. I’m glad you mentioned direnv, using it is actually part of my motivation for making the flake to begin with. What’s best is it’s compatible with zsh! As long as I can put everything in the flake, it should Just Work™ with anything direnv supports. Perhaps there’s a way to get the phases into the flake?

So, I guess my questions relating to the offset or dlopen() problem now are:

  1. How can I modify rpath inside mkShell with bespoke’s build system? (writeScriptBin might be useful?)
  2. Can I have nix build use build artifacts and possibly be less pedantic about cloning the git repo?
  3. If I can’t make dlopen() happy with mkShell, can I have the flake contain a docker container or something similar to put things in the correct paths? I know of a project called distrobox that might help with this, but it doesn’t have a way to be used declaratively.

And on making the flake better,

  1. Is there a way to get the phases from the package into the flake? If not, making our own with writeScriptBin doesn’t seem like a bad idea. Otherwise, people will just have to copy paste from the readme.

PS: oops I thought I sent this a few days ago! my bad.

One thing I was just thinking about is could we just use sed to replace the path that dlopen tries to use before build occurs and then change it back so git doesn’t report changes in the library? I’ll see if it works.

I will explain this later if needed as I need to leave, but long story short, this should work with zsh as well (it is compiling right now so I had no time yet to test the binary output):

{
  description = "A flake for developing bespoke synth";

  # Flake outputs
  outputs = { self, nixpkgs }:
    let
      # Systems supported
      allSystems = [
        "x86_64-linux" # 64-bit Intel/AMD Linux
        "aarch64-linux" # 64-bit ARM Linux
        "x86_64-darwin" # 64-bit Intel macOS
        "aarch64-darwin" # 64-bit ARM macOS
      ];

      # Helper to provide system-specific attributes
      forAllSystems = f: nixpkgs.lib.genAttrs allSystems (system: f {
        pkgs = import nixpkgs { inherit system; };
      });
    in
      {
        # Development environment output
        devShells = forAllSystems ({ pkgs }:
          let
            scripts = with pkgs; [
              (writeScriptBin "repo-path" ''
                ${pkgs.git}/bin/git rev-parse --show-toplevel
              '')
              (writeScriptBin "clean" ''
                rm -rf "$(repo-path)/cmake-build"
              '')
              (writeScriptBin "clean-release" ''
                rm -rf "$(repo-path)/cmake-build-release"
              '')
              (writeScriptBin "configure" ''
                export out=/tmp/out
                source $stdenv/setup
                export NIX_ENFORCE_PURITY=0
                phases="configurePhase" genericBuild
              '')
              (writeScriptBin "configure-release" ''
              cmake -DCMAKE_BUILD_TYPE=Release -B"$(repo-path)/cmake-build-release"
            '')
              (writeScriptBin "build" ''
                export out=/tmp/out
                source $stdenv/setup
                export NIX_ENFORCE_PURITY=0
                phases="buildPhase" genericBuild
              '')
              (writeScriptBin "build-release" ''
              configure-release
              cmake --build "$(repo-path)/cmake-build-release" --parallel "$(nproc)" --config Release
            '')
            ];
          in
            {
              # You can also create a mkShell by basically copying all fields of bespokesynth,
              # but it is easier to reuse stuff
              default = pkgs.bespokesynth.overrideAttrs (finalAttrs: previousAttrs: {
                # Bespokesynth should use this instead of patching LD_…
                NIX_LDFLAGS = "-rpath ${pkgs.lib.makeLibraryPath (with pkgs; with xorg; [ libX11 libXrandr libXinerama libXext libXcursor libXScrnSaver ])}";
                dontPatchELF = true; # needed or nix will try to optimize the binary by removing "useless" rpath   
                # We add some more packages here to quickly build the package
                buildInputs = previousAttrs.buildInputs ++ scripts;
              }); 
            });
      };
}

To use:

$ nix develop -c zsh
$ cd BespokeSynth # make sure to clone it with submodules
$ configure
$ cd build
$ build

This is mostly a proof of concept, but I think we should add this kind of package to nixpkgs to ease development as right now the user experience is really bad.

Proof:

1 Like

Also in mkShell you want buildInputs and/or inputsFrom, not packages since this one only brings the binaries without the source. mkShell is basically like a mkDerivation!

Ok, so I can confirm that the display bug is gone with the above flake, and either with the above “automatic” commands (note that when I tried this morning, the process was crashed at some point with the terminal near the end of the compilation, but I think it was because I had many app opened so it might have run out of memory):

or with the compilation commands recommended by the project online:

$ nix develop
$ cd BespokeSynth
$ cmake -Bignore/build -DCMAKE_BUILD_TYPE=Release
$ cmake --build ignore/build --parallel 4 --config Release
$ ./ignore/build/Source/BespokeSynth_artefacts/Release/BespokeSynth

(This should work with zsh the same way if you just add -c zsh)

image

Woah that’s really cool! Nice work! I tested both sets of steps and the second ones were able to get me a debug build without graphical issues! :partying_face:

I’m trying to understand why NIX_LDFLAGS fixes it. What I’m thinking is the linker was removing the search path for the needed libraries that get dlopened because it didn’t know they were needed (because of dlopen), so by setting the -rpath flag on the linker manually, we are telling it to include them regardless. Then, dontPatchELF makes it so the it doesn’t undo the steps performed earlier. That’s my best guess.

That has never occurred to me! Very interesting.

For some reason I these steps don’t to work on my machine? I tried the same directory structure you have, with the flake you in /tmp/bla and the bespoke source below it, then I ran each command.

This is the resulting error:

building
build flags: -j12
ninja: error: loading 'build.ninja': No such file or directory

This is a little confusing because bespoke normally doesn’t use ninja. I looked in the bespoke synth package and saw that it has ninja in nativeBuildInputs, which I never really understood. Do you know what’s going on with this?

I’m really glad it’s working on your machine though, at least it’s possible!

If you think it would be easier to message via matrix to debug the first set of steps, shoot me a message.

One other thing is would it be possible to have the things from nix develop be in the flake automatically? (Without having to remember to do nix develop?) That way direnv can keep it all simple.

Oh, and odd - it seems I can only build with the setup from /tmp? I tried moving it to my existing repository and things seemed to stop working. Could be a setup problem on my end, I’m investigating it.

Ok great! So regarding rpath (that actually sets RUNPATH since RPATH is deprecated now), every binary has some meta information inside the file helping it to find the libraries that might be in non-standard places. You can access them by inspecting the dynamic section of a binary:

$ readelf -d result/bin/demo
La section dynamique à l'offset 0x10dd8 contient 27 entrées :
  Étiquettes Type                         Nom/Valeur
 0x0000000000000001 (NEEDED)             Bibliothèque partagée: [libdl.so.2]
 0x0000000000000001 (NEEDED)             Bibliothèque partagée: [libc.so.6]
 0x000000000000001d (RUNPATH)            Bibliothèque runpath:[/nix/store/4nlgxhb09sdr51nc9hdm8az5b08vzkgx-glibc-2.35-163/lib]
…

(the exact order in which libraries are searched is in ld.so(8) - Linux manual page)

You can see here that this program asks for a library called libc.so.6, and is asked to search for libraries in /nix/store/4nlgxhb09sdr51nc9hdm8az5b08vzkgx-glibc-2.35-163/lib.

By default:

  • nix patches the linker ld so that during compile time, if the build command specified -L somelib to dynamically load, then somelib is used in the path (see where they find the list of libraries here and how they add rpath for these libraries here)
  • then, during the fixup phase, it removes all rpath not explicitely part of the dynamically loaded libraries (see NEEDED above, the code is here, see --shrink-path’s documentation)

So here, we need to disable this optimization with dontPatchELF = true;. Moreover, I guess that libX11 and co are maybe not given using -L, explaining why we need to explicitely add them (but you might want to double check, maybe removing the NIX_LDFLAGS would work as well. You can also use NIX_DEBUG=1 to really see what is going on).

Are you sure that you ran the cd build? I do have the build.ninja in the build folder. I got exactly the same error before. The reason you need it is that in theory, the cmake hook automatically goes to that folder but here since we run this inside a script, the path is reset at the end of the configure script. We should build a more resilient generic command that saves the state (certainly including environment variables and CWD) into a file that can be used by the next command… maybe a nice exercice, but it would be really helpful for people I guess.

I’m not an expert, but ninja is a cmake backend (basically, cmake outputs some build instructions, a bit like a Makefile but in better :stuck_out_tongue: into this build.ninja file: try to read it you will see!), so it is not something coming from Bespokesynth directly.

1 Like

I’m not sure to understand… if you want to do cd myfolder and have nix develop run automatically, then just configure direnv for flake. It should take what is in devShells.default by default as illustrated by their template https://github.com/nix-community/nix-direnv/blob/3f9e573b2ea862d944159dab7a6c199146c188ea/templates/flake/flake.nix.

Hum, I remember seeing something like that at some points, but the export NIX_ENFORCE_PURITY=0 line should already solve this issue, are you sure you have not removed it? This function indeed removes all path that are not in /nix/store or /tmp, in particular it would remove stuff in /home. I actually reported this bug here pkgs.mkShell: NIX_ENFORCE_PURITY is set during shellHook, but not afterwards · Issue #153298 · NixOS/nixpkgs · GitHub

PS: feel free to drop me a msg on matrix if this does not solve your issues, but I don’t guarantee that I can solve it ^^

1 Like

Aha! I figured out why it wasn’t working outside of /tmp - I never cleaned the build artifacts, which meant things hadn’t been re-linked.

Oops! My bad, I did miss that step :sweat_smile:. I’ll add it to the build script so it changes to $(repo-path)/build before running the build command. Maybe there’s a way to automatically know where it will place the build?

Oh, I guess I was really confused when I wrote that haha. This all makes sense now.


I guess the last step here is to figure out making debug builds. It can be done through the recommended build commands, but I’d be interested if it’s possible to do with the commands from the package (provided by nix develop).

Additionally, being able to change what build type is made on the fly (without having to edit the flake) would be nice. I currently have the recommended build commands set in another script list (scripts-recommended) that can be swapped in the buildInputs as alternatives that can be enabled via an environment variable, but if it’s possible to change the build type generated by the package’s commands (genericBuild), that would be optimal. Maybe this could be done by modifying stdenv or similar?

It’s what I explained above, to find it automatically in any case, the simpler is certainly to write in the hidden files the folder where the previous phase ended up… but for cmake it’s probably always the same so you approch is certainly good enough for now.

So in a derivation/flake you need to add:

cmakeBuildType = "Debug";

(pro tip: to quickly find this information, download nixpkgs locally, and use rg and fd to quicly search in the repo, e.g. here with rg "cmake.*Debug" you directly see some examples using this method, another round of rg cmakeBuildType shows you where it is used). So you can see that this is an environment variable (any stuff you write in mkDerivation endup in an env variable), and if this variable is not defined, then it uses Release. So to change this dynamically, you should be able to simply write in your shell:

$ export cmakeBuildType=Debug

(not tested)

No way! That is so clean! Tested and it works.

I think I’m going to submit a PR to the bespoke synth repo after I use it to make some PR’s. I’ll keep adding on to it before I submit it. I’d like to get apps implemented so it’s possible to use nix run, as well as packages for nix build. Maybe at some point this could even be used to build packages for other platforms and linux distros. (reminds me of this thread.)

Thanks for all your help making this, I have learned so much by working on it with you!

One thing I’m a little confused about is why does cmake change everything from /usr/local to /var/empty? It’s kind of annoying because git says there are changes but the changes aren’t something that should be committed. I tested on ubuntu (same distro used to build the nightly release) and it doesn’t do this.

Here’s the diff after configuring and building the project.

diff --git a/libs/CMakeLists.txt b/libs/CMakeLists.txt
index 8d357fa5..437973b8 100644
--- a/libs/CMakeLists.txt
+++ b/libs/CMakeLists.txt
@@ -33,8 +33,8 @@ if(NOT BESPOKE_SYSTEM_TUNING_LIBRARY)
   add_subdirectory(tuning-library EXCLUDE_FROM_ALL)
   message(STATUS "Using bundled tuning-library")
 else()
-  find_file(TUNING_LIB_TUNINGS NAMES Tunings.h PATHS /usr/local/include /usr/include REQUIRED)
-  find_file(TUNING_LIB_TUNINGS_IMPL NAMES TuningsImpl.h PATHS /usr/local/include /usr/include REQUIRED)
+  find_file(TUNING_LIB_TUNINGS NAMES Tunings.h PATHS /var/empty/local/include /var/empty/include REQUIRED)
+  find_file(TUNING_LIB_TUNINGS_IMPL NAMES TuningsImpl.h PATHS /var/empty/local/include /var/empty/include REQUIRED)
   message(STATUS "Using system provided tuning-library")
 endif()
 add_subdirectory(xwax EXCLUDE_FROM_ALL)
diff --git a/libs/json/jsoncpp b/libs/json/jsoncpp
--- a/libs/json/jsoncpp
+++ b/libs/json/jsoncpp
@@ -1 +1 @@
-Subproject commit 5defb4ed1a4293b8e2bf641e16b156fb9de498cc
+Subproject commit 5defb4ed1a4293b8e2bf641e16b156fb9de498cc-dirty
diff --git a/libs/pybind11 b/libs/pybind11
--- a/libs/pybind11
+++ b/libs/pybind11
@@ -1 +1 @@
-Subproject commit 5b632229a9f7d4bc8a2ac112a40249eb03b4e7f2
+Subproject commit 5b632229a9f7d4bc8a2ac112a40249eb03b4e7f2-dirty

Any idea what’s going on here?

UPDATE: I just found an issue that describes what’s going on. Apparently it’s old behaviour of nixpkgs, before something called sandbox existed. Adding dontFixCmake = true; to the shell fixes it.

https://github.com/NixOS/nixpkgs/pull/232522