21 Jul 2026
When you have a master Makefile and try to have a precise software environment per rule, it can become quite verbose.
In this post, we are looking at a way to integrate Guix and Nix into a Makefile in a not (so) verbose way.
We will consider the following scenario: a workflow with two rules each using their own captured environment: one for Python, one for R.
One way of implementing this in Guix would be to have one channels.scm and one manifest file for each of the rule (manifest-py.scm and manifest-r.scm).
Then, in the rule command, we would call guix time-machine -- shell to use the correct channels and manifest.
This might look something like this:
all: foo bar
CHANNELS:=channels.scm
foo: $(CHANNELS) manifest-py.scm
guix time-machine -C $(CHANNELS) -- shell -m manifest-py.scm -- python3 --version > $@
bar: $(CHANNELS) manifest-r.scm
guix time-machine -C $(CHANNELS) -- shell -m manifest-r.scm -- Rscript --version > $@
As you can see….. quite verbose.
We also need to add the channel and manifest files into the prerequisite list for make to rebuild the rule if the software environment changes.
However, these .scm file kind of get in the way of the “actual” prerequisites of the rules (which we might want to use/capture with @< and @^).
The main idea is to use the fact that some variables in Makefile are evaluated at “start time” (:=) and others at “use time” (=).
Thus, we set per-rule variable that will populate the global shell config which is evaluated a use time.
The Guix version might look something like this:
# Makefile
SHELL= guix
.SHELLFLAGS= time-machine -C $(CHANNELS) -- shell -m $(MANIFEST) -- bash -eu -o pipefail -c
CHANNELS:=channels.scm
all: foo bar
foo: MANIFEST=manifest-py.scm
foo: EXTRA_PREREQS=manifest-py.scm $(CHANNELS)
foo:
python3 --version > $@
bar: MANIFEST=manifest-R.scm
bar: EXTRA_PREREQS=manifest-R.scm $(CHANNELS)
bar:
Rscript --version > $@
So, the values of CHANNELS and MANIFEST only evaluated when calling the SHELL for running the rule.
We define those values either globally (like for CHANNELS), or at the rule level (like MANIFEST).
We also make sure make captures the dependencies between those Guix files and the rule.
To not “polute” the usual prerequisite list (i.e., those will not be in @^ and @<), we put them in the EXTRA_PREREQS variable.
The manifests are simply:
;; manifest-py.scm
(specifications->manifest (list "python" "bash"))
and
;; manifest-R.scm
(specifications->manifest (list "r" "bash"))
and channels.scm is the output from guix describe -f channels
The Nix version looks pretty similar.
Not that we are not using Flakes here.
(This is because of what we have discussed previously and because making the SHELL and .SHELLFLAGS usable with the nix develop command is a pain.)
# Makefile
SHELL= nix-shell
.SHELLFLAGS= --pure --arg channels $(CHANNELS) $(NIX_SHELL) --command
CHANNELS:=./channels.nix
all: foo bar
foo: NIX_SHELL=pyshell.nix
foo: EXTRA_PREREQS=pyshell.nix $(CHANNELS)
foo:
python3 --version > $@
bar: NIX_SHELL=rshell.nix
bar: EXTRA_PREREQS=rshell.nix $(CHANNELS)
bar:
Rscript --version > $@
where:
# channels.nix
import (builtins.fetchTarball {
url = "https://github.com/nixos/nixpkgs/archive/25.11.tar.gz";
sha256 = "sha256:1zn1lsafn62sz6azx6j735fh4vwwghj8cc9x91g5sx2nrg23ap9k";
})
# pyshell.nix
{ channels, pkgs ? import channels {} }:
pkgs.mkShell {
packages = with pkgs; [
python3
];
}
# rshell.nix
{ channels, pkgs ? import channels {} }:
pkgs.mkShell {
packages = with pkgs; [
R
];
}
Note that in the case of Nix, bash is already in the shell, so no need to explicit it.
But, I was not able to pass the -eu -o pipefail flags to the Nix bash in the .SHELLFLAGS as it seems we would need to capture the commands in the rule between quotes…