Resident (evidently unwelcome) nix evangelist chiming in here.
The article is about scripts that declare their own dependencies. I wrote another response lamenting the fact that we are talking about dependencies without mentioning IMO the best way to handle dependencies in a script and got flamed by a toxic luddite:
NIX is the answer.
Look at what actually gets declared in the article: the library version goes in the file, and the thing that runs the file is whatever brew install hands you that second. Going through OP’s examples:
Ruby: gem "optimist" has no version at all. Every machine resolves it to whatever is newest on first run.
Rust: cargo +nightly -Zscript. The +nightly syntax is a rustup proxy feature, so a distro-packaged cargo won’t even parse it. Nightly is a different compiler every day, -Zscript is unstable and its frontmatter syntax has already changed once, clap = "4" and anyhow = "1" are ranges, and there’s no Cargo.lock. Nothing about that script is fixed except its text.
Haskell: optparse-applicative is pinned exactly, but base >= 4.18 && < 5 accepts any GHC from 9.6 up, and the transitive deps get solved against whatever cabal update fetched. Two people running it a month apart get two build plans. You can add an index-state in a {- project: -} block, which helps with Hackage and does nothing for GHC.
Python: typer is exact, click and rich and the rest float, and >=3.10 is any interpreter. uv lock --script fixes that by writing a second file, at which point it’s no longer a single-file script.
Also env -S needs coreutils 8.30+ or a BSD env, and every prerequisite listed is Homebrew on macOS.
The biggest gap is that none of the examples touches a C library. The first script that needs libpq or openssl or zlib headers is outside what NuGet, Hackage, crates.io or PyPI can describe. You’re back to brew/apt and whatever version happens to be installed.
Nix has been usable as a shebang interpreter for YEARS, so here is the Haskell example with the compiler, every library, and libc pinned by one commit hash:
#!/usr/bin/env nix
#! nix shell --impure --expr ``
#! nix with (builtins.getFlake ''github:NixOS/nixpkgs/e554fab72f81915600f3f449b786fd9af40439a5'').legacyPackages.${builtins.currentSystem};
#! nix ghc.withPackages (ps: [ ps.optparse-applicative ])
#! nix ``
#! nix --command runghc
import Options.Applicative
data Options = Options
{ name :: String
}
options :: Parser Options
options =
Options
<$> strArgument
( metavar "NAME"
<> help "Name to greet"
)
main :: IO ()
main = do
opts <-
execParser $
info
(options <**> helper)
(fullDesc <> progDesc "Say hello")
putStrLn $ "Hello, " <> name opts <> "!"
--impure is only there so it can read the current system. Same GHC and same build of every dependency on any Linux or macOS box, today or in a hundred fifty years. Native deps are the same mechanism: add postgresql to the list.
When the library isn’t in nixpkgs, nix users pin the artifact by content hash. The article’s babashka example downloads org.babashka/cli from Clojars at runtime. Here the jar is a fixed-output fetch handed to bb on its classpath, so nothing gets resolved when the script runs:
The :reload matters: bb bundles its own copy of babashka.cli, and without it the require keeps the bundled one. If Clojars ever serves different bytes for that URL, the script refuses to run. This works for a jar with no transitive deps, which this one is. A real dependency tree needs a generated lock file, and at that point you want a flake.nix and flake.lock next to the script, with nix run replacing the shebang.
What you don’t get for free just by choosing Nix: you need Nix installed (one prerequisite instead of one per language), flakes are still behind an experimental flag upstream, first run is slow, evaluation adds a few hundred ms per run, and the five-line shebang is uglier than anything in the article. And if you write nixpkgs#babashka without a rev you’ve pinned nothing, it follows the registry.
So I’d say it belongs in the list. It’s the only entry where the interpreter is part of what the script declares.
Android is getting to the same point as Windows, just tried a Samsung a11+ and the native os appears hijacked by the manufacturer, but allows termux and termux-x11 to run Linux VMs
Resident (evidently unwelcome) nix evangelist chiming in here.
The article is about scripts that declare their own dependencies. I wrote another response lamenting the fact that we are talking about dependencies without mentioning IMO the best way to handle dependencies in a script and got flamed by a toxic luddite:
NIX is the answer.
Look at what actually gets declared in the article: the library version goes in the file, and the thing that runs the file is whatever
brew installhands you that second. Going through OP’s examples:Ruby:
gem "optimist"has no version at all. Every machine resolves it to whatever is newest on first run.Rust:
cargo +nightly -Zscript. The+nightlysyntax is a rustup proxy feature, so a distro-packaged cargo won’t even parse it. Nightly is a different compiler every day,-Zscriptis unstable and its frontmatter syntax has already changed once,clap = "4"andanyhow = "1"are ranges, and there’s no Cargo.lock. Nothing about that script is fixed except its text.Haskell: optparse-applicative is pinned exactly, but
base >= 4.18 && < 5accepts any GHC from 9.6 up, and the transitive deps get solved against whatevercabal updatefetched. Two people running it a month apart get two build plans. You can add anindex-statein a{- project: -}block, which helps with Hackage and does nothing for GHC.Python: typer is exact, click and rich and the rest float, and
>=3.10is any interpreter.uv lock --scriptfixes that by writing a second file, at which point it’s no longer a single-file script.Also
env -Sneeds coreutils 8.30+ or a BSD env, and every prerequisite listed is Homebrew on macOS.The biggest gap is that none of the examples touches a C library. The first script that needs libpq or openssl or zlib headers is outside what NuGet, Hackage, crates.io or PyPI can describe. You’re back to brew/apt and whatever version happens to be installed.
Nix has been usable as a shebang interpreter for YEARS, so here is the Haskell example with the compiler, every library, and libc pinned by one commit hash:
--impureis only there so it can read the current system. Same GHC and same build of every dependency on any Linux or macOS box, today or in a hundred fifty years. Native deps are the same mechanism: addpostgresqlto the list.When the library isn’t in nixpkgs, nix users pin the artifact by content hash. The article’s babashka example downloads
org.babashka/clifrom Clojars at runtime. Here the jar is a fixed-output fetch handed to bb on its classpath, so nothing gets resolved when the script runs:The
:reloadmatters: bb bundles its own copy of babashka.cli, and without it the require keeps the bundled one. If Clojars ever serves different bytes for that URL, the script refuses to run. This works for a jar with no transitive deps, which this one is. A real dependency tree needs a generated lock file, and at that point you want a flake.nix and flake.lock next to the script, withnix runreplacing the shebang.What you don’t get for free just by choosing Nix: you need Nix installed (one prerequisite instead of one per language), flakes are still behind an experimental flag upstream, first run is slow, evaluation adds a few hundred ms per run, and the five-line shebang is uglier than anything in the article. And if you write
nixpkgs#babashkawithout a rev you’ve pinned nothing, it follows the registry.So I’d say it belongs in the list. It’s the only entry where the interpreter is part of what the script declares.
How do I install Nix on Windows?
the same way I’d recommend installing bash, wsl
This is exactly what I do. Windows to me is just Linux with extra steps.
Android is getting to the same point as Windows, just tried a Samsung a11+ and the native os appears hijacked by the manufacturer, but allows termux and termux-x11 to run Linux VMs
Great question!
Here’s the set of instructions that I’d consider fairly canonical at this moment in time:
https://dev.to/jajera/using-nix-on-windows-the-right-way-14ki
That’s WSL though. Not really Windows…
You are welcome. Thank you for your service.
export RUSTC_BOOTSTRAP=1and you wouldn’t need rustup or nightly. It will work with your stable distro package.you can do
clap = "=4.x.x", ditto for all deps, direct and indirect.Is nix finished free-standing software?