That looks great! So let’s see each issue at a time:
Nix language
So to learn about the nix language you always have the nix manual and the Nix Pills that look more like a tutorial and are certainly easier to start with for the first two pages (you don’t need stuff regarding derivations).
You can also try to play with nix using the interpreter:
$ nix repl '<nixpkgs>'
nix-repl> 1+1
2
nix-repl> map (x: 2*x) [1 2 3]
[ 2 4 6 ]
nix-repl> pkgs.ripgrep.meta.mainProgram
"rg"
Importantly, Nix is a functional language, which is (roughly speaking) a programming paradigm in which functions play a central role, and in which you don’t have mutable variable (so variables can’t be changed like in imperative programming) or loops. Instead of loops, you use recursive functions (functions that call themselves)… or more often builtins functions like map
or fold
that implement the recursion for you (you really rarely need to code recursive functions in nix, most of the time use builtins functions that are available here, see also stuff in lib for more advanced stuff, all functions from builtins/lib are listed here as well). It has multiple advantages as it’s easier to automatically optimize code for parallelization (the compilator has more freedom to optimize your code, see eg Haskell), it also allows you to code stuff in a more concise way, you can use laziness to only evaluate an expression when you need it (this way infinite lists does make sense) and more…
To give maybe two examples of the most used constructions, the nix code:
map (x: 2*x) [1 2 3]
(that applies a function 2x
to a list and returns [2 4 6]
) is an equivalent of the python imperative code:
l = [1, 2, 3]
l2 = []
for x in l:
l2 += [x*2]
(note that python also allows some sort of functional programming, so in python you would use list comprehensions coming from Haskell like [2*x for x in l ]
to do the above map)
Similarly you can fake loops using:
foldl f acc list
which is doing the equivalent of the python code:
for x in list:
acc = f acc x
return acc
(acc
is called the accumulator as it accumulates the result of the previous loops) i.e:
foldl f acc [x_1 x_2 ... x_n] == f (... (f (f acc x_1) x_2) ... x_n)
To give a more concrete example, you could sum the elements of the lists using
nix-repl> lib.lists.fold (a: b: a+b) 0 [1 2 3]
6
as internally it will do ((0 + 1) + 2) + 3) = 6
since a: b: a+b
is the function that takes two arguments a
and b
and returns a+b
.
Similarly you could join multiple attribute sets using:
nix-repl> :p lib.foldl lib.recursiveUpdate {} [{a.b = 42;} {a.c = 44;}]
{ a = { b = 42; c = 44; }; }
(the :p
is just used in nix repl
to display the result)
since lib.recursiveUpdate A B
merges the two sets A
and B
.
Functional programming is not really ‘complex’ (everybody knows functions, right?), but it can be a bit disturbing to use when you are not used to it. You can find plenty of tutorials on functional programming online, see e.g. So You Want to be a Functional Programmer (Part 1) | by Charles Scalfani | Medium. In nix (and in many functional languages) I guess the most disturbing thing is that functions don’t have return
since it returns by default the last value, and the variable definitions are like let x = 5; in …
where …
ist a code block that can refer to x
.
Usage of listToAttr
So your goal is to rewrite this whole section into a code that automatically generate this same attribute set given [pkgs.ardour pkgs.audacity pkgs.bespokesynth …]
. Actually I’m thinking now that listToAttrs
is not even the best tool to use, you can first define next to your other definitions a function myCreatePackagesAppsShell
that creates the attribute for a single package (only partially tested):
nix-repl> myCreatePackagesAppsShell = myPackage: {
packages = flake-utils.lib.flattenTree { "${myPackage.pname}" = wrapProgram { programToWrap = myPackage; }; };
apps = { "${myPackage.pname}" = { type = "app"; program = getExecutablePath self.packages.${system}.${myPackage.pname}; }; }; # See below for definition of getExecutablePath
}
nix-repl> myCreatePackagesAppsShell pkgs.audacity
{ apps = { ... }; packages = { ... }; }
Then if you compute map myCreatePackagesAppsShell [ pkgs.audacity pkgs.ardour ]
you will get a list of attributes as described the line above: what you actually want is to merge all such attributes using something like that (put that directly in place of this whole code):
lib.lists.foldr lib.recursiveUpdate {} (map myCreatePackagesAppsShell myApps)
(see the above explaination of fold/foldr/foldl
to understand why it merges the sets, and you can find more ressources online like here)
You may have noticed that we did not considered yet the devShell
but it should be easy to add it as well using:
lib.lists.foldr lib.recursiveUpdate {} [
# Attribute set for the packages/app stuff
(map myCreatePackagesAppsShell myApps))
# Shell
{ devShell = pkgs.mkShell {
buildInputs = map (myPackage: self.packages.${system}.${myPackage.pname}) myApps ;
};
}
]
(not tested)
Name of programs: meta.mainProgram
(+overrideAttrs if needed)
Indeed, it can be an issue that programs don’t always have an executable name whose name is the name of the program. nix run
also has this issue and people introduced here a new way to deal with that case: programs that have a binary different from their own name should add an attribute meta.mainProgram
with the name of the binary. It is for instance the case of ripgrep or bespokesynth:
nix-repl> pkgs.bespokesynth.meta.mainProgram
"BespokeSynth"
Unfortunately not all programs follow this convention yet (looking at you ardour… maybe you can do your first contribution to nixpkgs to add that to ardour following the code in ripgrep!) If you still want to do that in your project without waiting for nixpkgs to accept your pull request, you can also override it locally:
let # in same let block as myApp…
myArdour = pkgs.ardour.overrideAttrs (finalAttrs: previousAttrs: { meta.mainProgram = "ardour6";});
# Apps = …
in # replace pkgs.ardour with myArdour
(note that it should not produce any recompilation, you can also inject it in an overlay, but it’s not even required, you can define it right before Apps
and just refer to myArdour
indeed of pkgs.ardour
later in your code)
You can then access this name using:
nix-repl> myArdour.meta.mainProgram
"ardour6"
Since many programs still use the pname
as the name of the binary (like audacity), you can create your function that picks meta.mainProgram
if it exists and fallback to pname
otherwise:
nix-repl> getExecutablePath = program: "${program}/bin/" + (program.meta.mainProgram or program.pname)
nix-repl> getExecutablePath pkgs.audacity
"/nix/store/q8vzngi38d1mgr8p22zkscff0pb29b1g-audacity-3.1.3/bin/audacity"
nix-repl> getExecutablePath myArdour
"/nix/store/r0my44qdja5mrfg08ihnj840cazr88y6-ardour-6.9/bin/ardour6"
Issues with path
I’m not sure to understand your issue: which ardour are you talking about: the one from nix run .#ardour
(which would be strange since you wrap it correctly as far as I see), or the one from the system (which is completely normal since you only locally change the environment variable in your wrapper, which don’t propagate to the rest of the system unless you take further actions for that)? Also, your oneliner has no chance to give any folder in /nix/store
for the reason mentioned above: the path are only defined in the wrapper of your program.
PS: nix --extra-experimental-features flakes --extra-experimental-features nix-command
can be shorten into nix --extra-experimental-features 'flakes nix-command'
.
PPS: regarding this comment you can either just replace the flake with .
this way it refers to the flake in the current folder (since you instruct you users to download the repository) or you can use git+https://codeberg.org/PowerUser/universal-studio.git
if you prefer them to always get the latest version (you may want to add --refresh
in the nix run
command in case it does not download the latest version automatically).