Announcing Lix 2.93 “Bici Bici”

May 6, 2025

We at the Lix team are proud to announce our fourth major release, version 2.93 “Bici Bici”.

This release focuses on bugfixes and continues integrating Lix with the KJ asynchronous runtime, in order to replace the previous bespoke implementation.

Bici bici is a Turkish crushed ice dessert made with starch and rose syrup.

Lix is a Nix implementation focused on reliability, predictability, friendliness, developed by a community of people from around the world. We have long term plans to incrementally evolve Nix to work in more places, to make it more reliable and secure, and to update the language and semantics to correct past mistakes and reduce errors, all the while providing an amazing tooling experience.

Upgrading from CppNix or previous Lix versions

The upgrade procedure depends on how you installed Lix or CppNix, and is fully described in the Lix installation guide.

If you are using Lix from nixpkgs on NixOS, you just need to upgrade your nixpkgs once the upgrade pull request has passed through the build farm into your channel; no other action is required.

If you want to help us test the next version of Lix, consider running main by following the beta guide.

Changes

Lix 2.93 continues the work from Lix 2.92 in improving the daemon and language to make room for future evolution.

Here are the highlights from the release notes. This is not a comprehensive list, and we are thankful for every contributor’s hard work in making this release happen.

Breaking changes

Two (anti-)features have been deprecated in Lix 2.93 and will produce errors by default. Deprecation errors can be suppressed with --extra-deprecated-features or any equivalent configuration option.

  • cr-line-endings: Using CR (\r) or CRLF (\r\n) line endings in Nix expressions is deprecated. The behavior of these line endings was inconsistent and broken and will lead to unexpected evaluation results with certain strings (e.g., an unescaped CR in a string literal would be silently translated to a newline, even if the rest of the file used LF line endings). Given that fixing the semantics might silently alter the evaluation result of derivations, the only option at the moment is to disallow them altogether. More proper support for CRLF is planned to be added back again in the future. Until then, all files must use \n exclusively.

  • nul-bytes: Literal NUL bytes (\0) in strings is now banned. Allowing these bytes in strings indirectly allows them in identifiers, which is unexpected. Note that it is still possible to introduce NUL bytes into Nix expressions without string literals; we’ll tighten down on these over time.

Thanks to piegames and eldritch horrors for this.

  • The recursive-nix experimental feature has been removed. This feature was primarily used to prototype dynamic derivations (dyndrvs), where build plans are generated on-the-fly during a build. There is currently no known usage of recursive-nix on lix or elsewhere in production. Thanks to raito.

  • Flake type = "file" inputs are now hashed recursively. This matches the behavior of CppNix 2.24 but breaks derivation hash stability with respect to Lix 2.92. This fixes an issue where builtins.fetchTree could return a different result if the file was available from a binary cache. Thanks to jade.

  • The repl-flake experimental feature has been removed and combined with the flake experimental feature. nix repl --file and --expr will continue to work without the flake experimental feature. Thanks to Jonathan De Troye and KFears.

  • nix-instantiate --parse prints a JSON representation of the internal expression tree instead of printing the AST in a Nix-like format. We’ve done our best to ensure that the new behavior is as compatible with the old one as possible, but tooling should not rely on the stdout of nix-instantiate --parse. If you depend on the old behavior in ways that are not covered anymore or are otherwise negatively affected by this change, then please reach out so that we can find a sustainable solution together. Thanks to piegames and eldritch horrors.

eBPF USDT/dtrace probes in Lix

Lix now has internal support for defining eBPF USDT/dtrace probes and has shipped its first probe. User-space statically defined tracing probes (USDT) allow instrumenting production systems with near-zero-cost for disabled probes. This makes it feasible to instrument hot paths in production builds.

Here we instrument the filetransfer__read probe in liblixstore.so to print when Lix reads a file from a binary cache:

$ sudo bpftrace -l 'usdt:/path/to/liblixstore.so:*:*'
usdt:/path/to/liblixstore.so:lix_store:filetransfer__read

$ sudo bpftrace -e 'usdt:*:lix_store:filetransfer__read { printf("%s read %d\n", str(arg0), arg1); }'
Attaching 1 probe...
https://cache.nixos.org/wvpzaycmvs39h5bcsfrxkjsg48mj4h73.narinf.. read 8192
https://cache.nixos.org/wvpzaycmvs39h5bcsfrxkjsg48mj4h73.narinf.. read 8192
https://cache.nixos.org/nar/1qshsc30nlarzdig0v9b1aasdkwaxhnv0a0.. read 65536
https://cache.nixos.org/nar/1qshsc30nlarzdig0v9b1aasdkwaxhnv0a0.. read 65536

Note that bpftrace does not offer any way to list the arguments to USDT probes in a human readable form. To get the probe definitions, see the *.d files in the Lix source code, for example, lix/libstore/trace-probes.d.

For more resources on eBPF/bpftrace and dtrace, see:

Improvements

  • A new lix CLI tool has been added, gated behind --extra-experimental-features lix-command. Running lix foo will invoke lix-foo from your $PATH, similar to how Git and Cargo integrate with other tools. The lix CLI does not provide any commands itself yet. Thanks to raito.

  • nix-env --install now accepts an optional --priority flag. This has the same function as the meta.priority derivation attribute, but can be used when installing a store path directly. Thanks to Andrew Hamon.

  • post-build-hook logs are now printed unconditionally. Previously, these logs were silently discarded unless print-build-logs was set, which is usually impractical for production use (large builds will produce enough output that GitHub Actions logs stop working, for example). Most usages of post-build-hook are pretty quiet (especially compared to build logs), so it should not be that bothersome to not be able to turn off. Thanks to jade.

  • Crashes are now reported to the syslog in addition to stderr, improving debuggability. Thanks to jade.

  • nix store diff-closures has gained a machine-readable --json output format. Thanks to Xavier Maso.

  • nix store sign is now parallelized with a thread pool, matching the behavior (and speed!) of nix store copy-sigs. Thanks to Lunaphied.

  • The default connect-timeout has been lowered to 5 (seconds) from 300 (5 minutes). This lets Lix detect unavailable substituters much quicker. Thanks to ma27.

Store path deletion

The ergonomics of nix-store --delete and nix store delete have been improved thanks to several contributions by lheckemann:

  • Lix will now attempt to delete all of the supplied paths before reporting any errors. Previously, the behavior depended on the lexical order (!) of the supplied paths.

  • --skip-live can now be provided to skip deleting any store paths that are reachable from a garbage collection root, rather than erroring out. (Contrast with --ignore-liveness, which skips checking if the store paths are still live and deletes them regardless.) As a result, paths known to be large can be thrown at nix store delete without having to manually filter out those that are still reachable from a root, e.g. nix store delete /nix/store/*mbrola-voices*

  • With the --delete-closure option, Lix can do more than delete individual paths. This is useful for paths that are not large themselves but do have a large closure size, e.g. nix store delete /nix/store/*nixos-system-gamingpc*.

  • Additionally, a regression initially introduced in Nix 2.5 (!) has been fixed. This caused nix-store --delete to fail when trying to delete a path that was still referenced by other paths, even if the referrers were not reachable from any GC roots. The old behaviour, where attempting to delete a store path would also delete its referrer closure, is now restored.

  • Finally, nix store delete will no longer realise installables specified on the command-line; previously, nix store delete nixpkgs#hello would download hello only to immediately delete it again. Now, it exits with an error if given an installable that isn’t in the store.

Better errors

  • When an attribute-set function is called without a mandatory argument, Lix will now show all missing and unexpected arguments, rather than just the first one.

    For example, attempting to evaluate ({ a, b, c } : a + b + c) { a = 1; d = 1; } will now produce this error:

    error: function 'anonymous lambda' called without required arguments 'b' and 'c' and with unexpected argument 'd'
    

    Thanks to Zitrone.

  • Lix now always prints hashes in the SRI base64 format (sha256-AAAA...). Previously, Lix would print hashes in the old base32 format (sha256:abcd...) in some cases. Thanks to jade.

  • Errors thrown when coercing an attribute set to a string via a __toString attribute now produce accurate stack traces in error messages. Thanks to eldritch horrors.

  • flake.lock parse failures no longer crash Lix and instead produce a nice error message:

    error:
           … while updating the lock file of flake 'git+file:///Users/jade/lix/lix2'
    
           … while parsing the lock file at /nix/store/mm5dqh8a729yazzj82cjffxl97n5c62s-source//flake.lock
    
           error: [json.exception.parse_error.101] parse error at line 1, column 1: syntax error while parsing value - invalid literal;
     last read: '#'
    

    Thanks to gilice.

  • Lix will no longer segfault when it receives invalid JSON.

    $ echo '{"puppy":' | nix derivation add
    warning: unknown experimental feature 'repl-flake'
    error:
           … while parsing a derivation from stdin
    
           error: failed to parse JSON: [json.exception.parse_error.101] parse error at line 2, column 1: syntax error while parsing value - unexpected end of input; expected '[', '{', or a literal
    

    Thanks to eldritch horrors.

  • Warning messages when an entry in the $NIX_PATH cannot be downloaded are now much more detailed. Previously, these warnings contained almost no context:

    warning: Nix search path entry 'https://example.com/404' cannot be downloaded, ignoring
    

    Now, the errors include HTTP status codes as well as the response body, which is very useful for debugging:

    warning:
         … while downloading https://example.com/404 to satisfy NIX_PATH lookup, ignoring search path entry
    
         warning: unable to download 'https://example.com/404': HTTP error 404 ()
    
         response body: […]
    

    Thanks to ma27.

REPL

The nix repl’s ergonomics have been improved thanks to a series of contributions from piegames:

  • Variable declarations can now optionally end with a semicolon, making it possible to copy-paste code from attribute sets.

  • Multiple declarations can be entered in one command, separated by semicolons.

  • Variable names are now parsed correctly; previously, legal identifiers such as strings would be rejected. Now, you can (e.g.) set "puppy" = 1.

  • The :env command, which prints a list of currently available variables, now works in nix repl as well as the --debugger.

  • The error message shown when a --debugger command is used in the nix repl has been improved. Previously, the REPL would claim that commands like :backtrace were “unknown”. Now, it will tell you that they’re only available in debug mode.

Fixes

As always, bugfixes and stability remain a top priority for Lix.

  • Canceling a build with Ctrl-C on macOS no longer leaves nix-daemon processes lingering.

    This was caused by a kernel bug jade discovered in macOS’s poll(2) implementation where it would forget about event subscriptions, breaking our detection of closed connections in the Lix daemon. We have rewritten the relevant thread to use kqueue(2), which is what the poll(2) implementation uses internally in the macOS kernel, so now Ctrl-C on clients will reliably terminate daemons once more. The file descriptor close monitoring code has had the highest Apple bug ID references per line of code anywhere in the project, and hopefully not using poll(2) will prevent us from hitting bugs in poll(2) in the future.

    Many thanks to jade for this, who embarked on a truly heroic debugging saga to figure out what was causing this. The eBPF USDT/dtrace probes added in this release are some of the fruits of this work as well!

  • --help output no longer contains garbled terminal escapes. Thanks to lheckemann.

  • Issues where the multiline progress bar would interfere with build output have been fixed. Make sure to try --log-format multiline-with-logs, it’s really good! Thanks to alois31 for this.

  • nix shell will now set IN_NIX_SHELL=impure (or pure if --ignore-environment is given), matching nix-shell -p and nix develop. This fixes an issue from 2020 that CppNix closed as WONTFIX. Thanks to Ersei Saggi.

  • A regression introduced in Lix 2.92 where nix config show --json would serialize deprecated and experimental features as integers instead of strings has been fixed. Thanks to eldritch horrors.

  • When the flakes experimental feature is disabled, builtins.fetchTree is no longer available. Previously, builtins.fetchTree was defined but would throw an uncatchable error when used if flakes were not enabled, making it impossible to determine if flakes are enabled from Nix code.

    builtins.fetchTree is the foundation of flake inputs and flake lock files, but is not fully specified in behaviour, which has led to regressions and behaviour differences with CppNix.

    This fixes a bug when using Eelco Dolstra’s version of flake-compat on Lix as well as a divergence with CppNix.

    Thanks to jade.

  • On the subject of fetchTree, calling builtins.fetchTree with a path and rev would produce a locked input, despite the fact that paths don’t have revisions. Now, inputs of type path, indirect and tarball are only considered locked when a narHash is specified. This corresponds to a change from CppNix 2.21 as well. Thanks to ma27.

  • Some soundness issues in nix store gc / nix-collect-garbage have been fixed. In particular, a bug in the file locking implementation has been fixed and garbage collection will no longer fail if a path fails to delete (this helps Lix tolerate desynchronization between the filesystem and database state). Thanks to eldritch horrors and raito.

  • A segfault with --debugger --ignore-try has been fixed. Now, enabling the ignore-try setting will once again properly disable the debugger within builtins.tryEval calls. Thanks to Dusk Banks.

  • A regression introduced in Lix 2.92 which made it possible to access ancestors of allowed paths in pure evaluation mode has been fixed. This made it possible to bypass the purity restrictions, for example by copying arbitrary files to the store with builtins.path { path = "/"; filter = ...; }. Thanks to alois31.

  • A macOS bug where the sandbox profile could exceed size limits when building derivations with many dependencies has been fixed. This resolves errors like:

     error: (failed with exit code 1, previous messages: sandbox initialization failed: data object length 65730 exceeds maximum (65535)|failed to configure sandbox)
    
            error: unexpected EOF reading a line
    

    Thanks to Pierre-Etienne Meunier and Poliorcetics.

  • Lix will no longer error out with error: input attribute 'lastModified' is not an integer if a tarball flake input URL redirects to a URL with a lastModified query parameter. Thanks to xanderio and Julian Stecklina for this.

  • Commands like nix build --eval-system x86_64-linux .#puppy will now correctly choose the packages.x86_64-linux.puppy attribute.

    Commands that actually run something on the local machine, like nix develop and nix run, are not impacted by this change.

    This is not a principled approach to cross compilation and we still believe that flakes impede rather than support cross compilation, but this unbreaks many remote build use cases.

    Thanks to jade.

  • The $NIX_CONFIG set for post-build-hooks now only includes settings which are set explicitly and excludes default values. This makes it possible to use CppNix in hooks, since CppNix does not support all of the configuration values Lix sets by default. Thanks to jade.

  • SSH connection sharing is no longer enabled by default, removing a buggy and untested “feature”. This may increase latency when the NIX_REMOTE or --store is set to a ssh:// or ssh-ng:// URL. Now, connection sharing is controlled by the user SSH config. Users who perform remote builds will likely want to make sure they have something like this in their ~/.ssh/config:

    Host my-cool-remote-builder.net
        ControlMaster auto
        ControlPath /tmp/ssh-%r@%h:%p
        ControlPersist 120
    

    Thanks to eldritch horrors.

  • A long-standing bug where checks for groups listed in trusted-users and allowed-users would incorrectly fail on macOS have been fixed.

    This fixes cases where the user’s PrimaryGroupID matches the group (like @staff), nested groups (like @_developer), and groups with synthesized membership (like @localaccounts).

    Thanks to lilyball.

  • nix-daemon now fetches the client PID on macOS, matching the behavior on Linux. As a result, the client PID will show up in the logs and pgrep -lf nix-daemon output will show client PIDs. Thanks to lilyball.

  • builtins.filterStore and builtins.path { filter = ...; } will now be passed logical paths when using chroot stores, rather than physical paths. Thanks to lily, alois31, and eldritch horrors.

You can read the full changelog in the manual.

Thanks, as always, to the following groups:

  • The several dozen people who beta tested the upcoming release by running main in production since the 2.93 branch-off. We really appreciate having immediate feedback on our work, and the trust of running main alongside us means a lot to us.

    If you want to run Lix main yourself, see the beta guide for details.

  • Everyone who contributed by filing bugs and giving us feedback on Matrix.

  • All the first time contributors who made their first contributions to a Nix implementation in Lix. We are eternally grateful to everyone who helped us out on the numerous important but tedious issues.

  • All the contributors who have helped us with the backlog of bugs.

  • The CppNix contributors and CppNix team, without whom we would not have this software, and who wrote some of the improvements ported into this release.

Onwards and upwards for the next release. We look forward to continuing to work together with everyone to build a better foundation for the evolution of Nix.