As best I can tell, Nix enthusiasts think that this is an XY problem and that I shouldn't want to pin individual tools/packages to arbitrary versions. But the thing is that I am a rude barbarian who very much does want to do this, however philosophically misguided it might be.
The downside is that flake inputs refer to other flakes, not individual packages, so if you update the nixpkgs input it will upgrade all of your packages at once. For some packages such as Python, nixpkgs tracks multiple major versions so you can loosely pin to that version. You can also include nixpkgs as an input multiple times under different git tags/commits and only use that input for some of your packages to effectively pin them. You could keep using one nixpkgs but override the package's source to build it for a specific version/commit, but this setup could break in the future, because the derivation (and therefore build instructions) will keep evolving while your package's version will not. Or, if you really wanted to, you could straight up just copy the derivation from nixpkgs into your local repository and use that instead.
Nix is quite flexible so there's more options than just these, it just takes a little getting used to to find out what's possible. I don't use devenv myself, but some quick googling reveals it works just fine with flakes, so I would try that to see if it suits your needs.
> How do I set up my development environment using devenv.sh to pin nodejs to 24.14.0?
If I understand your response correctly, I can't do this in any very practical way.
languages.javascript.enable = true
languages.javascript.package = pkgs.nodejs_24I’m not sure what other post you’re referring to. There’s one where you describe some approaches in broad terms, but as far as what the relevant devenv.sh config would look like, I’m none the wiser.
However, to actually address your question it would be something like this (with ruby also added for comparison of whats possible depending on the language ecosystem you are using)
```devenv.yaml```
inputs:
nixpkgs:
url: github:cachix/devenv-nixpkgs/rolling
nixpkgs-node:
url: github:nixos/nixpkgs?rev=9c0e2056b3c16190aafe67e4d29e530fc1f8c7c7
nixpkgs-ruby:
url: github:bobvanderlinden/nixpkgs-ruby
inputs:
nixpkgs:
follows: nixpkgs
```devenv.nix```
{ inputs, pkgs, ... }
{
languages.javascript = {
enable = true;
package = inputs.nixpkgs-node.nodejs_24;
};
languages.ruby = {
enable = true;
versionfile = ./.ruby-version;
// alternatively
// version = "3.2.4";
};
}Pinning by version number isn’t a crutch; it’s more a question of mental models. Most people find it more intuitive to identify a world by describing the things in it than to fix the states of the things by identifying the world where they have those states. Nix sometimes feels as if someone read Kripke and missed the passage about possible worlds being a figure of speech.
In other words, many of us don’t think that version numbers are a crutch to be used in the absence of a totalizing hash of the entire state of the universe. We actually think that version numbers are, for many practical purposes, better than that.
- one group of people who want guarantees that what they install on their machine matches on their colleague/CI/prod machines. For this group, the nix happy path basically serves all their needs really well. It can capture nearly any sort of software dependency they have regardless of ecosystem and it pins to actual content hashes in most cases. They only care that the high level versioning is met, because again its reproducibility that they care about
- one group of people want guarantees of specific software versions. Nix still handles this, but its "more work" because nix is by default a snapshot of the entire world, of which your target software is only a slice of. And most nix snapshots of the world are "what were the most recent versions of each software package at this point in time". So you generally need to compose together multiple snapshots of the world. It works fine, its just more work (and more disk space).
In practice, again, I think most teams are served better by being in group one, but dipping into group two when they either need an "unreleased" version for software they depend on or when they need an older/unsupported version of software they depend on (or a nix release breaks their workflow). Then nix shines by being able to mix and match between different snapshots in composable/reproducible ways.
~/repos/nixpkgs$ git log --grep='nodejs.* 24.14.0' -1 origin/master
commit 9c0e2056b3c16190aafe67e4d29e530fc1f8c7c7
Merge: d3a4e93b79c9 0873aea2d0da
Date: Tue Feb 24 16:53:40 2026 +0000
nodejs_24: 24.13.1 -> 24.14.0 (#493691)
~/repos/nixpkgs$ nix eval nixpkgs/9c0e2056b3c16190aafe67e4d29e530fc1f8c7c7#nodejs_24.version
"24.14.0"
~/repos/nixpkgs$In general though when working on a team, you dont really care about the _exact semver version_, you care about the major and handle version bumps by bumping the pin of nix packages.
The way to do it is to find the `nixpkgs` version which contains the version of the tool you care about. There's a web site[1] that makes this pretty easy, and it's of course also doable by looking at the Git history for the program's derivation.
Then you create a named input using that nixpkgs version: either add it as a channel, import it with fetchTarball in a derivation, or add it as an input in your flake, depending on what you're doing. Then you use that named nixpkgs (or other input in the flake case) for that version of the package.
Edit: One issue with depending on things like git tags or semver versions is that sometimes people re-use versions or edit tags. Using the actual git commit hashes of the package's derivation avoids this potential ambiguity. This is why we can't have nice things.
{ pkgs }:
pkgs.mkShell {
nativeBuildInputs = with pkgs; [
# build tools
cmake
ninja
gnumake
pkg-config
];
buildInputs = with pkgs; [
# java
jdk8
# compilers
gcc
clang
llvmPackages.libcxx
# libraries
capstone
icu
openssl_3
libusb1
libftdi
zlib
# scripting
(python3.withPackages (ps: with ps; [
requests
pyelftools
]))
];
# capstone headers are in include/capstone/ but blutter expects include/
shellHook = ''
export CPATH="${pkgs.capstone}/include/capstone:$CPATH"
export CPLUS_INCLUDE_PATH="${pkgs.capstone}/include/capstone:$CPLUS_INCLUDE_PATH"
'';
}Btw, I say this as a huge fan and heavy user of both Nix and NixOS.
Modules let you express the system in smaller, composable, reusable parts rather than express everything in one big file. (There are other popular tools which support modules: NixOS, home-manager, flake-parts).
That devenv also provides "batteries included" modules for popular languages (including linters, LSPs) is also a benefit.
devenv does not do any user-level change (you will not be able to make it configure your WM), but works at the directory level.
For instance I'm currently working on a Rust + C++ project, and my devenv, whenever I enter this project folder: make CMake/g++/cargo/cbindgen available, enable a couple scripts to longer CMake invokations, set-up everything required for C++ and Rust LSPs, and create a couple git hooks to validate formatting etc.