├── .envrc
├── .github
├── dependabot.yml
└── workflows
│ ├── auto-merge.yaml
│ ├── test.yml
│ └── update-flake-lock.yml
├── .gitignore
├── LICENSE
├── README.md
├── default.nix
├── direnvrc
├── flake.lock
├── flake.nix
├── pyproject.toml
├── scripts
└── create-release.sh
├── shell.nix
├── templates
└── flake
│ ├── .envrc
│ └── flake.nix
├── test-runner.nix
├── tests
├── __init__.py
├── conftest.py
├── direnv_project.py
├── procs.py
├── root.py
├── test_gc.py
├── test_use_nix.py
└── testenv
│ ├── flake.lock
│ ├── flake.nix
│ └── shell.nix
└── treefmt.nix
/.envrc:
--------------------------------------------------------------------------------
1 | # shellcheck shell=bash
2 | strict_env
3 | source ./direnvrc
4 | watch_file direnvrc ./*.nix
5 | use flake
6 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 |
--------------------------------------------------------------------------------
/.github/workflows/auto-merge.yaml:
--------------------------------------------------------------------------------
1 | name: Auto Merge Dependency Updates
2 | on:
3 | - pull_request_target
4 | jobs:
5 | auto-merge-dependency-updates:
6 | runs-on: ubuntu-latest
7 | permissions:
8 | contents: write
9 | pull-requests: write
10 | concurrency:
11 | group: "auto-merge:${{ github.head_ref }}"
12 | cancel-in-progress: true
13 | steps:
14 | - uses: Mic92/auto-merge@main
15 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: "Test"
2 | on:
3 | pull_request:
4 | merge_group:
5 | push:
6 | branches:
7 | - master
8 | - staging
9 | - trying
10 | jobs:
11 | tests:
12 | strategy:
13 | matrix:
14 | os: [ubuntu-latest]
15 | # FIXME macos garbage currently collect also nix-shell that runs the test
16 | #os: [ ubuntu-latest, macos-latest ]
17 | variants: [stable, latest]
18 | runs-on: ${{ matrix.os }}
19 | steps:
20 | - uses: actions/checkout@v4
21 | - uses: cachix/install-nix-action@v31
22 | with:
23 | nix_path: nixpkgs=channel:nixpkgs-unstable
24 | extra_nix_config: |
25 | experimental-features = nix-command flakes
26 | - run: "nix run --accept-flake-config .#test-runner-${{ matrix.variants }}"
27 |
--------------------------------------------------------------------------------
/.github/workflows/update-flake-lock.yml:
--------------------------------------------------------------------------------
1 | name: update-flake-lock
2 | on:
3 | workflow_dispatch: # allows manual triggering
4 | schedule:
5 | - cron: "0 0 * * 1,4" # Run twice a week
6 | permissions:
7 | pull-requests: write
8 | contents: write
9 | jobs:
10 | lockfile:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout repository
14 | uses: actions/checkout@v4
15 | - name: Install Nix
16 | uses: cachix/install-nix-action@v31
17 | with:
18 | github_access_token: ${{ secrets.GITHUB_TOKEN }}
19 | - uses: actions/create-github-app-token@v2
20 | id: app-token
21 | with:
22 | app-id: ${{ vars.CI_APP_ID }}
23 | private-key: ${{ secrets.CI_APP_PRIVATE_KEY }}
24 | - name: Update flake.lock
25 | uses: DeterminateSystems/update-flake-lock@v25
26 | with:
27 | token: ${{ steps.app-token.outputs.token }}
28 | pr-body: |
29 | Automated changes by the update-flake-lock
30 | ```
31 | {{ env.GIT_COMMIT_MESSAGE }}
32 | ```
33 | pr-labels: | # Labels to be set on the PR
34 | auto-merge
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .direnv/
2 | /template/flake.lock
3 |
4 | # Byte-compiled / optimized / DLL files
5 | __pycache__/
6 | *.py[cod]
7 | *$py.class
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Nix community projects
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # nix-direnv
2 |
3 | 
4 |
5 | A faster, persistent implementation of `direnv`'s `use_nix` and `use_flake`, to
6 | replace the built-in one.
7 |
8 | Prominent features:
9 |
10 | - significantly faster after the first run by caching the `nix-shell`
11 | environment
12 | - prevents garbage collection of build dependencies by symlinking the resulting
13 | shell derivation in the user's `gcroots` (Life is too short to lose your
14 | project's build cache if you are on a flight with no internet connection)
15 |
16 | ## Why not use `lorri` instead?
17 |
18 | Compared to [lorri](https://github.com/nix-community/lorri), nix-direnv is
19 | simpler (and requires no external daemon). Additionally, lorri can sometimes
20 | re-evaluate the entirety of nixpkgs on every change (leading to perpetual high
21 | CPU load).
22 |
23 | ## Installation
24 |
25 | Requirements:
26 |
27 | - bash 4.4
28 | - nix 2.4 or newer
29 | - direnv 2.21.3 or newer
30 |
31 | > [!WARNING]\
32 | > We assume that [direnv](https://direnv.net/) is installed properly because
33 | > nix-direnv IS NOT a replacement for regular direnv _(only some of its
34 | > functionality)_.
35 |
36 | > [!NOTE]\
37 | > nix-direnv requires a modern Bash. MacOS ships with bash 3.2 from 2007. As a
38 | > work-around we suggest that macOS users install `direnv` via Nix or Homebrew.
39 | > There are different ways to install nix-direnv, pick your favourite:
40 |
41 |
42 | Via home-manager (Recommended)
43 |
44 | ### Via home-manager
45 |
46 | Note that while the home-manager integration is recommended, some use cases
47 | require the use of features only present in some versions of nix-direnv. It is
48 | much harder to control the version of nix-direnv installed with this method. If
49 | you require such specific control, please use another method of installing
50 | nix-direnv.
51 |
52 | In `$HOME/.config/home-manager/home.nix` add
53 |
54 | ```Nix
55 | {
56 | # ...other config, other config...
57 |
58 | programs = {
59 | direnv = {
60 | enable = true;
61 | enableBashIntegration = true; # see note on other shells below
62 | nix-direnv.enable = true;
63 | };
64 |
65 | bash.enable = true; # see note on other shells below
66 | };
67 | }
68 | ```
69 |
70 | Check the current
71 | [Home Manager Options](https://mipmip.github.io/home-manager-option-search/?query=direnv)
72 | for integration with shells other than Bash. Be sure to also allow
73 | `home-manager` to manage your shell with `programs..enable = true`.
74 |
75 |
76 |
77 | Direnv's source_url
78 |
79 | ### Direnv source_url
80 |
81 | Put the following lines in your `.envrc`:
82 |
83 | ```bash
84 | if ! has nix_direnv_version || ! nix_direnv_version 3.1.0; then
85 | source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.1.0/direnvrc" "sha256-yMJ2OVMzrFaDPn7q8nCBZFRYpL/f0RcHzhmw/i6btJM="
86 | fi
87 | ```
88 |
89 |
90 |
91 |
92 | Via system configuration on NixOS
93 |
94 | ### Via system configuration on NixOS
95 |
96 | For NixOS 23.05+ all that's required is
97 |
98 | ```Nix
99 | {
100 | programs.direnv.enable = true;
101 | }
102 | ```
103 |
104 | other available options are:
105 |
106 | ```Nix
107 | { pkgs, ... }: {
108 | #set to default values
109 | programs.direnv = {
110 | package = pkgs.direnv;
111 | silent = false;
112 | loadInNixShell = true;
113 | direnvrcExtra = "";
114 | nix-direnv = {
115 | enable = true;
116 | package = pkgs.nix-direnv;
117 | };
118 | }
119 | ```
120 |
121 |
122 |
123 |
124 | With `nix profile`
125 |
126 | ### With `nix profile`
127 |
128 | As **non-root** user do the following:
129 |
130 | ```shell
131 | nix profile install nixpkgs#nix-direnv
132 | ```
133 |
134 | Then add nix-direnv to `$HOME/.config/direnv/direnvrc`:
135 |
136 | ```bash
137 | source $HOME/.nix-profile/share/nix-direnv/direnvrc
138 | ```
139 |
140 |
141 |
142 |
143 | From source
144 |
145 | ### From source
146 |
147 | Clone the repository to some directory and then source the direnvrc from this
148 | repository in your own `~/.config/direnv/direnvrc`:
149 |
150 | ```bash
151 | # put this in ~/.config/direnv/direnvrc
152 | source $HOME/nix-direnv/direnvrc
153 | ```
154 |
155 |
156 |
157 | ## Usage example
158 |
159 | Either add `shell.nix` or a `default.nix` to the project directory:
160 |
161 | ```nix
162 | # save this as shell.nix
163 | { pkgs ? import {}}:
164 |
165 | pkgs.mkShell {
166 | packages = [ pkgs.hello ];
167 | }
168 | ```
169 |
170 | Then add the line `use nix` to your envrc:
171 |
172 | ```shell
173 | echo "use nix" >> .envrc
174 | direnv allow
175 | ```
176 |
177 | If you haven't used direnv before, make sure to
178 | [hook it into your shell](https://direnv.net/docs/hook.html) first.
179 |
180 | ### Using a non-standard file name
181 |
182 | You may use a different file name than `shell.nix` or `default.nix` by passing
183 | the file name in `.envrc`, e.g.:
184 |
185 | ```shell
186 | echo "use nix foo.nix" >> .envrc
187 | ```
188 |
189 | ## Flakes support
190 |
191 | nix-direnv also comes with an alternative `use_flake` implementation. The code
192 | is tested and does work but the upstream flake api is not finalized, so we
193 | cannot guarantee stability after a nix upgrade.
194 |
195 | Like `use_nix`, our `use_flake` will prevent garbage collection of downloaded
196 | packages, including flake inputs.
197 |
198 | ### Creating a new flake-native project
199 |
200 | This repository ships with a
201 | [flake template](https://github.com/nix-community/nix-direnv/tree/master/templates/flake).
202 | which provides a basic flake with devShell integration and a basic `.envrc`.
203 |
204 | To make use of this template, you may issue the following command:
205 |
206 | ```shell
207 | nix flake new -t github:nix-community/nix-direnv
208 | ```
209 |
210 | ### Integrating with a existing flake
211 |
212 | ```shell
213 | echo "use flake" >> .envrc && direnv allow
214 | ```
215 |
216 | The `use flake` line also takes an additional arbitrary flake parameter, so you
217 | can point at external flakes as follows:
218 |
219 | ```bash
220 | use flake ~/myflakes#project
221 | ```
222 |
223 | ### Advanced usage
224 |
225 | #### use flake
226 |
227 | Under the covers, `use_flake` calls `nix print-dev-env`. The first argument to
228 | the `use_flake` function is the flake expression to use, and all other arguments
229 | are proxied along to the call to `print-dev-env`. You may make use of this fact
230 | for some more arcane invocations.
231 |
232 | For instance, if you have a flake that needs to be called impurely under some
233 | conditions, you may wish to pass `--impure` to the `print-dev-env` invocation so
234 | that the environment of the calling shell is passed in.
235 |
236 | You can do that as follows:
237 |
238 | ```shell
239 | echo "use flake . --impure" > .envrc
240 | direnv allow
241 | ```
242 |
243 | #### use nix
244 |
245 | Like `use flake`, `use nix` now uses `nix print-dev-env`. Due to historical
246 | reasons, the argument parsing emulates `nix shell`.
247 |
248 | This leads to some limitations in what we can reasonably parse.
249 |
250 | Currently, all single-word arguments and some well-known double arguments will
251 | be interpreted or passed along.
252 |
253 | #### Manual reload of the nix environment
254 |
255 | To avoid delays and time consuming rebuilds at unexpected times, you can use
256 | nix-direnv in the "manual reload" mode. nix-direnv will then tell you when the
257 | nix environment is no longer up to date. You can then decide yourself when you
258 | want to reload the nix environment.
259 |
260 | To activate manual mode, use `nix_direnv_manual_reload` in your `.envrc` like
261 | this:
262 |
263 | ```shell
264 | nix_direnv_manual_reload
265 | use nix # or use flake
266 | ```
267 |
268 | To reload your nix environment, use the `nix-direnv-reload` command:
269 |
270 | ```shell
271 | nix-direnv-reload
272 | ```
273 |
274 | ##### Known arguments
275 |
276 | - `-p`: Starts a list of packages to install; consumes all remaining arguments
277 | - `--include` / `-I`: Add the following path to the list of lookup locations for
278 | `<...>` file names
279 | - `--attr` / `-A`: Specify the output attribute to utilize
280 |
281 | `--command`, `--run`, `--exclude`, `--pure`, `-i`, and `--keep` are explicitly
282 | ignored.
283 |
284 | All single word arguments (`-j4`, `--impure` etc) are passed to the underlying
285 | nix invocation.
286 |
287 | #### Tracked files
288 |
289 | As a convenience, `nix-direnv` adds common files to direnv's watched file list
290 | automatically.
291 |
292 | The list of additionally tracked files is as follows:
293 |
294 | - for `use nix`:
295 | - `~/.direnvrc`
296 | - `~/.config/direnv/direnvrc`
297 | - `.envrc`,
298 | - A single nix file. In order of preference:
299 | - The file argument to `use nix`
300 | - `default.nix` if it exists
301 | - `shell.nix` if it exists
302 |
303 | - for `use flake`:
304 | - `~/.direnvrc`
305 | - `~/.config/direnv/direnvrc`
306 | - `.envrc`
307 | - `flake.nix`
308 | - `flake.lock`
309 | - `devshell.toml` if it exists
310 |
311 | Users are free to use direnv's builtin `watch_file` function to track additional
312 | files. `watch_file` must be invoked before either `use flake` or `use nix` to
313 | take effect.
314 |
315 | #### Environment Variables
316 |
317 | nix-direnv sets the following environment variables for user consumption. All
318 | other environment variables are either a product of the underlying nix
319 | invocation or are purely incidental and should not be relied upon.
320 |
321 | - `NIX_DIRENV_DID_FALLBACK`: Set when the current revision of your nix shell or
322 | flake's devShell are invalid and nix-direnv has loaded the last known working
323 | shell.
324 |
325 | nix-direnv also respects the following environment variables for configuration.
326 |
327 | - `NIX_DIRENV_FALLBACK_NIX`: Can be set to a fallback Nix binary location, to be
328 | used when a compatible one isn't available in `PATH`. Defaults to
329 | `config.nix.package` if installed via the NixOS module, otherwise needs to be
330 | set manually. Leave unset or empty to fail immediately when a Nix
331 | implementation can't be found on `PATH`.
332 |
333 | ## General direnv tips
334 |
335 | - [Changing where direnv stores its cache][cache_location]
336 | - [Quickly setting up direnv in a new nix project][new_project]
337 | - [Disable the diff notice (requires direnv 2.34+)][hide_diff_notice]: Note that
338 | this goes into direnv's TOML configuration!
339 |
340 | [cache_location]: https://github.com/direnv/direnv/wiki/Customizing-cache-location
341 | [new_project]: https://github.com/nix-community/nix-direnv/wiki/Shell-integration
342 | [hide_diff_notice]: https://direnv.net/man/direnv.toml.1.html#codehideenvdiffcode
343 |
344 | ## Other projects in the field
345 |
346 | - [lorri](https://github.com/nix-community/lorri)
347 | - [sorri](https://github.com/nmattia/sorri)
348 | - [nixify](https://github.com/kalbasit/nur-packages/blob/master/pkgs/nixify/envrc)
349 | - [lorelei](https://github.com/shajra/direnv-nix-lorelei)
350 |
--------------------------------------------------------------------------------
/default.nix:
--------------------------------------------------------------------------------
1 | {
2 | resholve,
3 | lib,
4 | coreutils,
5 | nix,
6 | writeText,
7 | }:
8 |
9 | # resholve does not yet support `finalAttrs` call pattern hence `rec`
10 | # https://github.com/abathur/resholve/issues/107
11 | resholve.mkDerivation rec {
12 | pname = "nix-direnv";
13 | version = "3.1.0";
14 |
15 | src = builtins.path {
16 | path = ./.;
17 | name = pname;
18 | };
19 |
20 | installPhase = ''
21 | install -m400 -D direnvrc $out/share/${pname}/direnvrc
22 | '';
23 |
24 | solutions = {
25 | default = {
26 | scripts = [ "share/${pname}/direnvrc" ];
27 | interpreter = "none";
28 | inputs = [ coreutils ];
29 | fake = {
30 | builtin = [
31 | "PATH_add"
32 | "direnv_layout_dir"
33 | "has"
34 | "log_error"
35 | "log_status"
36 | "watch_file"
37 | ];
38 | function = [
39 | # not really a function - this is in an else branch for macOS/homebrew that
40 | # cannot be reached when built with nix
41 | "shasum"
42 | ];
43 | external = [
44 | # We want to reference the ambient Nix when possible, and have custom logic
45 | # for the fallback
46 | "nix"
47 | ];
48 | };
49 | keep = {
50 | "$cmd" = true;
51 | "$direnv" = true;
52 |
53 | # Nix fallback implementation
54 | "$_nix_direnv_nix" = true;
55 | "$ambient_nix" = true;
56 | "$NIX_DIRENV_FALLBACK_NIX" = true;
57 | };
58 | prologue =
59 | (writeText "prologue.sh" ''
60 | NIX_DIRENV_SKIP_VERSION_CHECK=1
61 | NIX_DIRENV_FALLBACK_NIX=''${NIX_DIRENV_FALLBACK_NIX:-${lib.getExe nix}}
62 | '').outPath;
63 | };
64 | };
65 |
66 | meta = with lib; {
67 | description = "A fast, persistent use_nix implementation for direnv";
68 | homepage = "https://github.com/nix-community/nix-direnv";
69 | license = licenses.mit;
70 | platforms = platforms.unix;
71 | };
72 | }
73 |
--------------------------------------------------------------------------------
/direnvrc:
--------------------------------------------------------------------------------
1 | # -*- mode: sh -*-
2 | # shellcheck shell=bash
3 |
4 | NIX_DIRENV_VERSION=3.1.0
5 |
6 | # min required versions
7 | BASH_MIN_VERSION=4.4
8 | DIRENV_MIN_VERSION=2.21.3
9 |
10 | _NIX_DIRENV_LOG_PREFIX="nix-direnv: "
11 |
12 | _nix_direnv_info() {
13 | log_status "${_NIX_DIRENV_LOG_PREFIX}$*"
14 | }
15 |
16 | _nix_direnv_warning() {
17 | local msg=$*
18 | local color_normal=""
19 | local color_warning=""
20 |
21 | if [[ -t 2 ]]; then
22 | color_normal="\e[m"
23 | color_warning="\e[33m"
24 | fi
25 |
26 | printf "%b" "$color_warning"
27 | log_status "${_NIX_DIRENV_LOG_PREFIX}${msg}"
28 | printf "%b" "$color_normal"
29 | }
30 |
31 | _nix_direnv_error() { log_error "${_NIX_DIRENV_LOG_PREFIX}$*"; }
32 |
33 | _nix_direnv_nix=""
34 |
35 | _nix() {
36 | ${_nix_direnv_nix} --no-warn-dirty --extra-experimental-features "nix-command flakes" "$@"
37 | }
38 |
39 | _require_version() {
40 | local cmd=$1 version=$2 required=$3
41 | if ! printf "%s\n" "$required" "$version" | LC_ALL=C sort -c -V 2>/dev/null; then
42 | _nix_direnv_error \
43 | "minimum required $(basename "$cmd") version is $required (installed: $version)"
44 | return 1
45 | fi
46 | }
47 |
48 | _require_cmd_version() {
49 | local cmd=$1 required=$2 version
50 | if ! has "$cmd"; then
51 | _nix_direnv_error "command not found: $cmd"
52 | return 1
53 | fi
54 | version=$($cmd --version)
55 | [[ $version =~ ([0-9]+\.[0-9]+\.[0-9]+) ]]
56 | _require_version "$cmd" "${BASH_REMATCH[1]}" "$required"
57 | }
58 |
59 | _nix_direnv_preflight() {
60 | if [[ -z $direnv ]]; then
61 | # shellcheck disable=2016
62 | _nix_direnv_error '$direnv environment variable was not defined. Was this script run inside direnv?'
63 | return 1
64 | fi
65 |
66 | # check command min versions
67 | if [[ -z ${NIX_DIRENV_SKIP_VERSION_CHECK:-} ]]; then
68 | # bash check uses $BASH_VERSION with _require_version instead of
69 | # _require_cmd_version because _require_cmd_version uses =~ operator which would be
70 | # a syntax error on bash < 3
71 | if ! _require_version bash "$BASH_VERSION" "$BASH_MIN_VERSION" ||
72 | # direnv stdlib defines $direnv
73 | ! _require_cmd_version "$direnv" "$DIRENV_MIN_VERSION"; then
74 | return 1
75 | fi
76 | fi
77 |
78 | if command -v nix >/dev/null 2>&1; then
79 | _nix_direnv_nix=$(command -v nix)
80 | elif [[ -n ${NIX_DIRENV_FALLBACK_NIX:-} ]]; then
81 | _nix_direnv_nix="${NIX_DIRENV_FALLBACK_NIX}"
82 | else
83 | _nix_direnv_error "Could not find Nix binary, please add Nix to PATH or set NIX_DIRENV_FALLBACK_NIX"
84 | return 1
85 | fi
86 |
87 | local layout_dir
88 | layout_dir=$(direnv_layout_dir)
89 |
90 | if [[ ! -d "$layout_dir/bin" ]]; then
91 | mkdir -p "$layout_dir/bin"
92 | fi
93 | # N.B. This script relies on variable expansion in *this* shell.
94 | # (i.e. The written out file will have the variables expanded)
95 | # If the source path changes, the script becomes broken.
96 | # Because direnv_layout_dir is user controlled,
97 | # we can't assume to be able to reverse it to get the source dir
98 | # So there's little to be done about this.
99 | cat >"${layout_dir}/bin/nix-direnv-reload" <<-EOF
100 | #!/usr/bin/env bash
101 | set -e
102 | if [[ ! -d "$PWD" ]]; then
103 | echo "Cannot find source directory; Did you move it?"
104 | echo "(Looking for "$PWD")"
105 | echo 'Cannot force reload with this script - use "direnv reload" manually and then try again'
106 | exit 1
107 | fi
108 |
109 | # rebuild the cache forcefully
110 | _nix_direnv_force_reload=1 direnv exec "$PWD" true
111 |
112 | # Update the mtime for .envrc.
113 | # This will cause direnv to reload again - but without re-building.
114 | touch "$PWD/.envrc"
115 |
116 | # Also update the timestamp of whatever profile_rc we have.
117 | # This makes sure that we know we are up to date.
118 | touch -r "$PWD/.envrc" "${layout_dir}"/*.rc
119 | EOF
120 |
121 | chmod +x "${layout_dir}/bin/nix-direnv-reload"
122 |
123 | PATH_add "${layout_dir}/bin"
124 | }
125 |
126 | # Usage: nix_direnv_version
127 | #
128 | # Checks that the nix-direnv version is at least as old as .
129 | nix_direnv_version() {
130 | _require_version nix-direnv $NIX_DIRENV_VERSION "$1"
131 | }
132 |
133 | _nix_export_or_unset() {
134 | local key=$1 value=$2
135 | if [[ $value == __UNSET__ ]]; then
136 | unset "$key"
137 | else
138 | export "$key=$value"
139 | fi
140 | }
141 |
142 | _nix_import_env() {
143 | local profile_rc=$1
144 |
145 | local -A values_to_restore=(
146 | ["NIX_BUILD_TOP"]=${NIX_BUILD_TOP:-__UNSET__}
147 | ["TMP"]=${TMP:-__UNSET__}
148 | ["TMPDIR"]=${TMPDIR:-__UNSET__}
149 | ["TEMP"]=${TEMP:-__UNSET__}
150 | ["TEMPDIR"]=${TEMPDIR:-__UNSET__}
151 | ["terminfo"]=${terminfo:-__UNSET__}
152 | )
153 | local old_xdg_data_dirs=${XDG_DATA_DIRS:-}
154 |
155 | # On the first run in manual mode, the profile_rc does not exist.
156 | if [[ ! -e $profile_rc ]]; then
157 | return
158 | fi
159 |
160 | eval "$(<"$profile_rc")"
161 | # `nix print-dev-env` will create a temporary directory and use it as TMPDIR
162 | # We cannot rely on this directory being available at all times,
163 | # as it may be garbage collected.
164 | # Instead - just remove it immediately.
165 | # Use recursive & force as it may not be empty.
166 | if [[ -n ${NIX_BUILD_TOP+x} && $NIX_BUILD_TOP == */nix-shell.* && -d $NIX_BUILD_TOP ]]; then
167 | rm -rf "$NIX_BUILD_TOP"
168 | fi
169 |
170 | for key in "${!values_to_restore[@]}"; do
171 | _nix_export_or_unset "$key" "${values_to_restore[${key}]}"
172 | done
173 |
174 | local new_xdg_data_dirs=${XDG_DATA_DIRS:-}
175 | export XDG_DATA_DIRS=
176 | local IFS=:
177 | for dir in $new_xdg_data_dirs${old_xdg_data_dirs:+:}$old_xdg_data_dirs; do
178 | dir="${dir%/}" # remove trailing slashes
179 | if [[ :$XDG_DATA_DIRS: == *:$dir:* ]]; then
180 | continue # already present, skip
181 | fi
182 | XDG_DATA_DIRS="$XDG_DATA_DIRS${XDG_DATA_DIRS:+:}$dir"
183 | done
184 | }
185 |
186 | _nix_add_gcroot() {
187 | local storepath=$1
188 | local symlink=$2
189 | _nix build --out-link "$symlink" "$storepath"
190 | }
191 |
192 | _nix_clean_old_gcroots() {
193 | local layout_dir=$1
194 |
195 | rm -rf "$layout_dir/flake-inputs/"
196 | rm -f "$layout_dir"/{nix,flake}-profile*
197 | }
198 |
199 | _nix_argsum_suffix() {
200 | local out checksum
201 | if [ -n "$1" ]; then
202 |
203 | if has sha1sum; then
204 | out=$(sha1sum <<<"$1")
205 | elif has shasum; then
206 | out=$(shasum <<<"$1")
207 | else
208 | # degrade gracefully both tools are not present
209 | return
210 | fi
211 | read -r checksum _ <<<"$out"
212 | echo "-$checksum"
213 | fi
214 | }
215 |
216 | nix_direnv_watch_file() {
217 | # shellcheck disable=2016
218 | log_error '`nix_direnv_watch_file` is deprecated - use `watch_file`'
219 | watch_file "$@"
220 | }
221 |
222 | _nix_direnv_watches() {
223 | local -n _watches=$1
224 | if [[ -z ${DIRENV_WATCHES-} ]]; then
225 | return
226 | fi
227 | while IFS= read -r line; do
228 | local regex='"[Pp]ath": "(.+)"$'
229 | if [[ $line =~ $regex ]]; then
230 | local path="${BASH_REMATCH[1]}"
231 | if [[ $path == "${XDG_DATA_HOME:-${HOME:-/var/empty}/.local/share}/direnv/allow/"* ]]; then
232 | continue
233 | fi
234 | # expand new lines and other json escapes
235 | # shellcheck disable=2059
236 | path=$(printf "$path")
237 | _watches+=("$path")
238 | fi
239 | done < <($direnv show_dump "${DIRENV_WATCHES}")
240 | }
241 |
242 | : "${_nix_direnv_manual_reload:=0}"
243 | nix_direnv_manual_reload() {
244 | _nix_direnv_manual_reload=1
245 | }
246 |
247 | _nix_direnv_warn_manual_reload() {
248 | if [[ -e $1 ]]; then
249 | _nix_direnv_warning 'cache is out of date. use "nix-direnv-reload" to reload'
250 | else
251 | _nix_direnv_warning 'cache does not exist. use "nix-direnv-reload" to create it'
252 | fi
253 | }
254 |
255 | use_flake() {
256 | if ! _nix_direnv_preflight; then
257 | return 1
258 | fi
259 |
260 | flake_expr="${1:-.}"
261 | flake_uri="${flake_expr%#*}"
262 | flake_dir=${flake_uri#"path:"}
263 |
264 | if [[ $flake_expr == -* ]]; then
265 | local message="the first argument must be a flake expression"
266 | if [[ -n ${2:-} ]]; then
267 | _nix_direnv_error "$message"
268 | return 1
269 | else
270 | _nix_direnv_error "$message. did you mean 'use flake . $1'?"
271 | return 1
272 | fi
273 | fi
274 |
275 | local files_to_watch
276 | files_to_watch=("$HOME/.direnvrc" "$HOME/.config/direnv/direnvrc")
277 |
278 | if [[ -d $flake_dir ]]; then
279 | files_to_watch+=("$flake_dir/flake.nix" "$flake_dir/flake.lock" "$flake_dir/devshell.toml")
280 | fi
281 |
282 | watch_file "${files_to_watch[@]}"
283 |
284 | local layout_dir profile
285 | layout_dir=$(direnv_layout_dir)
286 | profile="${layout_dir}/flake-profile$(_nix_argsum_suffix "$flake_expr")"
287 | local profile_rc="${profile}.rc"
288 | local flake_inputs="${layout_dir}/flake-inputs/"
289 |
290 | local need_update=0
291 | local watches
292 | _nix_direnv_watches watches
293 | local file=
294 | for file in "${watches[@]}"; do
295 | if [[ $file -nt $profile_rc ]]; then
296 | need_update=1
297 | break
298 | fi
299 | done
300 |
301 | if [[ ! -e $profile ||
302 | ! -e $profile_rc ||
303 | $need_update -eq 1 ]] \
304 | ; then
305 | if [[ $_nix_direnv_manual_reload -eq 1 && -z ${_nix_direnv_force_reload-} ]]; then
306 | _nix_direnv_warn_manual_reload "$profile_rc"
307 |
308 | else
309 | local tmp_profile_rc
310 | local tmp_profile="${layout_dir}/flake-tmp-profile.$$"
311 | if tmp_profile_rc=$(_nix print-dev-env --profile "$tmp_profile" "$@"); then
312 | # If we've gotten here, the user's current devShell is valid and we should cache it
313 | _nix_clean_old_gcroots "$layout_dir"
314 |
315 | # We need to update our cache
316 | echo "$tmp_profile_rc" >"$profile_rc"
317 | _nix_add_gcroot "$tmp_profile" "$profile"
318 | rm -f "$tmp_profile" "$tmp_profile"*
319 |
320 | # also add garbage collection root for source
321 | local flake_input_paths
322 | mkdir -p "$flake_inputs"
323 | flake_input_paths=$(_nix flake archive \
324 | --json --no-write-lock-file \
325 | -- "$flake_uri")
326 |
327 | while [[ $flake_input_paths =~ /nix/store/[^\"]+ ]]; do
328 | local store_path="${BASH_REMATCH[0]}"
329 | _nix_add_gcroot "${store_path}" "${flake_inputs}/${store_path##*/}"
330 | flake_input_paths="${flake_input_paths/${store_path}/}"
331 | done
332 |
333 | _nix_direnv_info "Renewed cache"
334 | else
335 | # The user's current flake failed to evaluate,
336 | # but there is already a prior profile_rc,
337 | # which is probably more useful than nothing.
338 | # Fallback to use that (which means just leaving profile_rc alone!)
339 | _nix_direnv_warning "Evaluating current devShell failed. Falling back to previous environment!"
340 | export NIX_DIRENV_DID_FALLBACK=1
341 | fi
342 | fi
343 | else
344 | if [[ -e ${profile_rc} ]]; then
345 | # Our cache is valid, use that
346 | _nix_direnv_info "Using cached dev shell"
347 | else
348 | # We don't have a profile_rc to use!
349 | _nix_direnv_error "use_flake failed - Is your flake's devShell working?"
350 | return 1
351 | fi
352 | fi
353 |
354 | _nix_import_env "$profile_rc"
355 | }
356 |
357 | use_nix() {
358 | if ! _nix_direnv_preflight; then
359 | return 1
360 | fi
361 |
362 | local layout_dir path version
363 | layout_dir=$(direnv_layout_dir)
364 | if path=$(_nix eval --impure --expr "" 2>/dev/null); then
365 | if [[ -f "${path}/.version-suffix" ]]; then
366 | version=$(<"${path}/.version-suffix")
367 | elif [[ -f "${path}/.git/HEAD" ]]; then
368 | local head
369 | read -r head <"${path}/.git/HEAD"
370 | local regex="ref: (.*)"
371 | if [[ $head =~ $regex ]]; then
372 | read -r version <"${path}/.git/${BASH_REMATCH[1]}"
373 | else
374 | version="$head"
375 | fi
376 | elif [[ -f "${path}/.version" && ${path} == "/nix/store/"* ]]; then
377 | # borrow some bits from the store path
378 | local version_prefix
379 | read -r version_prefix < <(
380 | cat "${path}/.version"
381 | echo
382 | )
383 | version="${version_prefix}-${path:11:16}"
384 | fi
385 | fi
386 |
387 | local profile
388 | profile="${layout_dir}/nix-profile-${version:-unknown}$(_nix_argsum_suffix "$*")"
389 | local profile_rc="${profile}.rc"
390 |
391 | local in_packages=0
392 | local attribute=
393 | local packages=""
394 | local extra_args=()
395 |
396 | local nixfile=
397 | if [[ -e "shell.nix" ]]; then
398 | nixfile="./shell.nix"
399 | elif [[ -e "default.nix" ]]; then
400 | nixfile="./default.nix"
401 | fi
402 |
403 | while [[ $# -gt 0 ]]; do
404 | i="$1"
405 | shift
406 |
407 | case $i in
408 | -p | --packages)
409 | in_packages=1
410 | ;;
411 | --command | --run | --exclude)
412 | # These commands are unsupported
413 | # ignore them
414 | shift
415 | ;;
416 | --pure | -i | --keep)
417 | # These commands are unsupported (but take no argument)
418 | # ignore them
419 | ;;
420 | --include | -I)
421 | extra_args+=("$i" "${1:-}")
422 | shift
423 | ;;
424 | --attr | -A)
425 | attribute="${1:-}"
426 | shift
427 | ;;
428 | --option | -o | --arg | --argstr)
429 | extra_args+=("$i" "${1:-}" "${2:-}")
430 | shift
431 | shift
432 | ;;
433 | -*)
434 | # Other arguments are assumed to be of a single arg form
435 | # (--foo=bar or -j4)
436 | extra_args+=("$i")
437 | ;;
438 | *)
439 | if [[ $in_packages -eq 1 ]]; then
440 | packages+=" $i"
441 | else
442 | nixfile=$i
443 | fi
444 | ;;
445 | esac
446 | done
447 |
448 | watch_file "$HOME/.direnvrc" "$HOME/.config/direnv/direnvrc" "shell.nix" "default.nix"
449 |
450 | local need_update=0
451 | local watches
452 | _nix_direnv_watches watches
453 | local file=
454 | for file in "${watches[@]}"; do
455 | if [[ $file -nt $profile_rc ]]; then
456 | need_update=1
457 | break
458 | fi
459 | done
460 |
461 | if [[ ! -e $profile ||
462 | ! -e $profile_rc ||
463 | $need_update -eq 1 ]] \
464 | ; then
465 | if [[ $_nix_direnv_manual_reload -eq 1 && -z ${_nix_direnv_force_reload-} ]]; then
466 | _nix_direnv_warn_manual_reload "$profile_rc"
467 | else
468 | local tmp_profile="${layout_dir}/nix-tmp-profile.$$"
469 | local tmp_profile_rc
470 | if [[ -n $packages ]]; then
471 | extra_args+=("--expr" "with import {}; mkShell { buildInputs = [ $packages ]; }")
472 | else
473 | extra_args+=("--file" "$nixfile")
474 | if [[ -n $attribute ]]; then
475 | extra_args+=("$attribute")
476 | fi
477 | fi
478 |
479 | # Some builtin nix tooling depends on this variable being set BEFORE their invocation to change their behavior
480 | # (notably haskellPackages.developPackage returns an env if this is set)
481 | # This allows us to more closely mimic nix-shell.
482 | export IN_NIX_SHELL="impure"
483 |
484 | if tmp_profile_rc=$(_nix \
485 | print-dev-env \
486 | --profile "$tmp_profile" \
487 | --impure \
488 | "${extra_args[@]}"); then
489 | _nix_clean_old_gcroots "$layout_dir"
490 |
491 | echo "$tmp_profile_rc" >"$profile_rc"
492 | _nix_add_gcroot "$tmp_profile" "$profile"
493 | rm -f "$tmp_profile" "$tmp_profile"*
494 | _nix_direnv_info "Renewed cache"
495 | else
496 | _nix_direnv_warning "Evaluating current nix shell failed. Falling back to previous environment!"
497 | export NIX_DIRENV_DID_FALLBACK=1
498 | fi
499 | fi
500 | else
501 | if [[ -e ${profile_rc} ]]; then
502 | _nix_direnv_info "Using cached dev shell"
503 | else
504 | _nix_direnv_error "use_nix failed - Is your nix shell working?"
505 | return 1
506 | fi
507 | fi
508 |
509 | _nix_import_env "$profile_rc"
510 |
511 | }
512 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-parts": {
4 | "inputs": {
5 | "nixpkgs-lib": [
6 | "nixpkgs"
7 | ]
8 | },
9 | "locked": {
10 | "lastModified": 1743550720,
11 | "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=",
12 | "owner": "hercules-ci",
13 | "repo": "flake-parts",
14 | "rev": "c621e8422220273271f52058f618c94e405bb0f5",
15 | "type": "github"
16 | },
17 | "original": {
18 | "owner": "hercules-ci",
19 | "repo": "flake-parts",
20 | "type": "github"
21 | }
22 | },
23 | "nixpkgs": {
24 | "locked": {
25 | "lastModified": 1748186667,
26 | "narHash": "sha256-UQubDNIQ/Z42R8tPCIpY+BOhlxO8t8ZojwC9o2FW3c8=",
27 | "owner": "NixOS",
28 | "repo": "nixpkgs",
29 | "rev": "bdac72d387dca7f836f6ef1fe547755fb0e9df61",
30 | "type": "github"
31 | },
32 | "original": {
33 | "owner": "NixOS",
34 | "ref": "nixpkgs-unstable",
35 | "repo": "nixpkgs",
36 | "type": "github"
37 | }
38 | },
39 | "root": {
40 | "inputs": {
41 | "flake-parts": "flake-parts",
42 | "nixpkgs": "nixpkgs",
43 | "treefmt-nix": "treefmt-nix"
44 | }
45 | },
46 | "treefmt-nix": {
47 | "inputs": {
48 | "nixpkgs": [
49 | "nixpkgs"
50 | ]
51 | },
52 | "locked": {
53 | "lastModified": 1747912973,
54 | "narHash": "sha256-XgxghfND8TDypxsMTPU2GQdtBEsHTEc3qWE6RVEk8O0=",
55 | "owner": "numtide",
56 | "repo": "treefmt-nix",
57 | "rev": "020cb423808365fa3f10ff4cb8c0a25df35065a3",
58 | "type": "github"
59 | },
60 | "original": {
61 | "owner": "numtide",
62 | "repo": "treefmt-nix",
63 | "type": "github"
64 | }
65 | }
66 | },
67 | "root": "root",
68 | "version": 7
69 | }
70 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "A faster, persistent implementation of `direnv`'s `use_nix`, to replace the built-in one.";
3 |
4 | inputs = {
5 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
6 | flake-parts = {
7 | url = "github:hercules-ci/flake-parts";
8 | inputs.nixpkgs-lib.follows = "nixpkgs";
9 | };
10 | treefmt-nix = {
11 | url = "github:numtide/treefmt-nix";
12 | inputs.nixpkgs.follows = "nixpkgs";
13 | };
14 | };
15 |
16 | outputs =
17 | inputs@{ flake-parts, ... }:
18 | flake-parts.lib.mkFlake { inherit inputs; } (
19 | { lib, ... }:
20 | {
21 | imports = [ ./treefmt.nix ];
22 | systems = [
23 | "aarch64-linux"
24 | "x86_64-linux"
25 |
26 | "x86_64-darwin"
27 | "aarch64-darwin"
28 | ];
29 | perSystem =
30 | {
31 | config,
32 | pkgs,
33 | self',
34 | ...
35 | }:
36 | {
37 | packages = {
38 | nix-direnv = pkgs.callPackage ./default.nix { };
39 | default = config.packages.nix-direnv;
40 | test-runner-stable = pkgs.callPackage ./test-runner.nix { nixVersion = "stable"; };
41 | test-runner-latest = pkgs.callPackage ./test-runner.nix { nixVersion = "latest"; };
42 | };
43 |
44 | devShells.default = pkgs.callPackage ./shell.nix {
45 | packages = [
46 | config.treefmt.build.wrapper
47 | pkgs.shellcheck
48 | ];
49 | };
50 |
51 | checks =
52 | let
53 | packages = lib.mapAttrs' (n: lib.nameValuePair "package-${n}") self'.packages;
54 | devShells = lib.mapAttrs' (n: lib.nameValuePair "devShell-${n}") self'.devShells;
55 | in
56 | packages // devShells;
57 | };
58 | flake = {
59 | overlays.default = final: _prev: { nix-direnv = final.callPackage ./default.nix { }; };
60 | templates.default = {
61 | path = ./templates/flake;
62 | description = "nix flake new -t github:nix-community/nix-direnv .";
63 | };
64 | };
65 | }
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.ruff]
2 | target-version = "py311"
3 | line-length = 88
4 | select = ["ALL"]
5 | ignore = [
6 | # pydocstyle
7 | "D",
8 | # Missing type annotation for `self` in method
9 | "ANN101",
10 | # Trailing comma missing
11 | "COM812",
12 | # Unnecessary `dict` call (rewrite as a literal)
13 | "C408",
14 | # Boolean-typed positional argument in function definition
15 | "FBT001",
16 | # Logging statement uses f-string
17 | "G004",
18 | # disabled on ruff's recommendation as causes problems with the formatter
19 | "ISC001",
20 | # Use of `assert` detected
21 | "S101",
22 | # `subprocess` call: check for execution of untrusted input
23 | "S603",
24 |
25 | # FIXME? Maybe we should enable these?
26 | "PLR0913", # Too many arguments in function definition (7 > 5)
27 | "PLR2004", # Magic value used in comparison, consider replacing 4 with a constant variable
28 | "FBT002", # Boolean default positional argument in function definition
29 | ]
30 |
31 | [tool.mypy]
32 | python_version = "3.10"
33 | warn_redundant_casts = true
34 | disallow_untyped_calls = true
35 | disallow_untyped_defs = true
36 | no_implicit_optional = true
37 |
38 | [[tool.mypy.overrides]]
39 | module = "setuptools.*"
40 | ignore_missing_imports = true
41 |
42 | [[tool.mypy.overrides]]
43 | module = "pytest.*"
44 | ignore_missing_imports = true
45 |
--------------------------------------------------------------------------------
/scripts/create-release.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -eu -o pipefail
4 |
5 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
6 | cd "$SCRIPT_DIR/.."
7 |
8 | version=${1:-}
9 | if [[ -z $version ]]; then
10 | echo "USAGE: $0 version" 2>/dev/null
11 | exit 1
12 | fi
13 |
14 | if [[ "$(git symbolic-ref --short HEAD)" != "master" ]]; then
15 | echo "must be on master branch" 2>/dev/null
16 | exit 1
17 | fi
18 |
19 | waitForPr() {
20 | local pr=$1
21 | while true; do
22 | if gh pr view "$pr" | grep -q 'MERGED'; then
23 | break
24 | fi
25 | echo "Waiting for PR to be merged..."
26 | sleep 5
27 | done
28 | }
29 |
30 | sed -Ei "s!(version = ).*!\1\"$version\";!" default.nix
31 | sed -Ei "s!(NIX_DIRENV_VERSION=).*!\1$version!" direnvrc
32 |
33 | sed -i README.md templates/flake/.envrc \
34 | -e 's!\(nix-direnv/\).*\(/direnvrc\)!\1'"${version}"'\2!' \
35 | -e 's?\( ! nix_direnv_version \)[0-9.]\+\(; \)?\1'"${version}"'\2?'
36 | git add README.md direnvrc templates/flake/.envrc default.nix
37 | git commit -m "bump version ${version}"
38 | git tag "${version}"
39 | git branch -D "release-${version}" || true
40 | git checkout -b "release-${version}"
41 | git push origin --force "release-${version}"
42 | gh pr create \
43 | --base master \
44 | --head "release-${version}" \
45 | --title "Release ${version}" \
46 | --body "Release ${version} of nix-direnv"
47 |
48 | gh pr merge --auto "release-${version}"
49 |
50 | waitForPr "release-${version}"
51 | git push origin "$version"
52 |
53 | sha256=$(direnv fetchurl "https://raw.githubusercontent.com/nix-community/nix-direnv/${version}/direnvrc" | grep -m1 -o 'sha256-.*')
54 | sed -i README.md templates/flake/.envrc -e "s!sha256-.*!${sha256}\"!"
55 | git add README.md templates/flake/.envrc
56 | git commit -m "update fetchurl checksum"
57 | git push origin --force "release-${version}"
58 | gh pr create \
59 | --base master \
60 | --head "release-${version}" \
61 | --title "Update checksums for release ${version} of nix-direnv" \
62 | --body "Update checksums for release ${version} of nix-direnv"
63 | gh pr merge --auto "release-${version}"
64 | waitForPr "release-${version}"
65 |
66 | echo "You can now create a release at https://github.com/nix-community/nix-direnv/releases for version ${version}"
67 |
--------------------------------------------------------------------------------
/shell.nix:
--------------------------------------------------------------------------------
1 | {
2 | pkgs ? import { },
3 | packages ? [ ],
4 | }:
5 |
6 | with pkgs;
7 | mkShell {
8 | packages = packages ++ [
9 | python3.pkgs.pytest
10 | python3.pkgs.mypy
11 | ruff
12 | direnv
13 | ];
14 | }
15 |
--------------------------------------------------------------------------------
/templates/flake/.envrc:
--------------------------------------------------------------------------------
1 | # shellcheck shell=bash
2 | if ! has nix_direnv_version || ! nix_direnv_version 3.1.0; then
3 | source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.1.0/direnvrc" "sha256-yMJ2OVMzrFaDPn7q8nCBZFRYpL/f0RcHzhmw/i6btJM="
4 | fi
5 | use flake
6 |
--------------------------------------------------------------------------------
/templates/flake/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "A basic flake with a shell";
3 | inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
4 | inputs.systems.url = "github:nix-systems/default";
5 | inputs.flake-utils = {
6 | url = "github:numtide/flake-utils";
7 | inputs.systems.follows = "systems";
8 | };
9 |
10 | outputs =
11 | { nixpkgs, flake-utils, ... }:
12 | flake-utils.lib.eachDefaultSystem (
13 | system:
14 | let
15 | pkgs = nixpkgs.legacyPackages.${system};
16 | in
17 | {
18 | devShells.default = pkgs.mkShell { packages = [ pkgs.bashInteractive ]; };
19 | }
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/test-runner.nix:
--------------------------------------------------------------------------------
1 | {
2 | writeShellScriptBin,
3 | direnv,
4 | python3,
5 | lib,
6 | coreutils,
7 | gnugrep,
8 | nixVersions,
9 | nixVersion,
10 | }:
11 | writeShellScriptBin "test-runner-${nixVersion}" ''
12 | set -e
13 | export PATH=${
14 | lib.makeBinPath [
15 | direnv
16 | nixVersions.${nixVersion}
17 | coreutils
18 | gnugrep
19 | ]
20 | }
21 |
22 | echo run unittest
23 | ${lib.getExe' python3.pkgs.pytest "pytest"} .
24 | ''
25 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nix-community/nix-direnv/5e729f239f5a3b1a95bfe69d66ccb01a8a01d5b1/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import pytest
4 |
5 | pytest_plugins = [
6 | "tests.direnv_project",
7 | "tests.root",
8 | ]
9 |
10 |
11 | @pytest.fixture(autouse=True)
12 | def _cleanenv(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
13 | # so direnv doesn't touch $HOME
14 | monkeypatch.setenv("HOME", str(tmp_path / "home"))
15 | # so direnv allow state writes under tmp HOME
16 | monkeypatch.delenv("XDG_DATA_HOME", raising=False)
17 | # so direnv does not pick up user customization
18 | monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
19 |
--------------------------------------------------------------------------------
/tests/direnv_project.py:
--------------------------------------------------------------------------------
1 | import shutil
2 | import textwrap
3 | from collections.abc import Iterator
4 | from dataclasses import dataclass
5 | from pathlib import Path
6 | from tempfile import TemporaryDirectory
7 |
8 | import pytest
9 |
10 | from .procs import run
11 |
12 |
13 | @dataclass
14 | class DirenvProject:
15 | directory: Path
16 | nix_direnv: Path
17 |
18 | @property
19 | def envrc(self) -> Path:
20 | return self.directory / ".envrc"
21 |
22 | def setup_envrc(self, content: str, strict_env: bool) -> None:
23 | text = textwrap.dedent(
24 | f"""
25 | {"strict_env" if strict_env else ""}
26 | source {self.nix_direnv}
27 | {content}
28 | """
29 | )
30 | self.envrc.write_text(text)
31 | run(["direnv", "allow"], cwd=self.directory)
32 |
33 |
34 | @pytest.fixture
35 | def direnv_project(test_root: Path, project_root: Path) -> Iterator[DirenvProject]:
36 | """
37 | Setups a direnv test project
38 | """
39 | with TemporaryDirectory() as _dir:
40 | directory = Path(_dir) / "proj"
41 | shutil.copytree(test_root / "testenv", directory)
42 | nix_direnv = project_root / "direnvrc"
43 |
44 | c = DirenvProject(Path(directory), nix_direnv)
45 | try:
46 | yield c
47 | finally:
48 | pass
49 |
--------------------------------------------------------------------------------
/tests/procs.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import shlex
3 | import subprocess
4 | from pathlib import Path
5 | from typing import IO, Any
6 |
7 | _FILE = None | int | IO[Any]
8 | _DIR = None | Path | str
9 |
10 | log = logging.getLogger(__name__)
11 |
12 |
13 | def run(
14 | cmd: list[str],
15 | text: bool = True,
16 | check: bool = True,
17 | cwd: _DIR = None,
18 | stderr: _FILE = None,
19 | stdout: _FILE = None,
20 | env: dict[str, str] | None = None,
21 | ) -> subprocess.CompletedProcess:
22 | if cwd is not None:
23 | log.debug(f"cd {cwd}")
24 | log.debug(f"$ {shlex.join(cmd)}")
25 | return subprocess.run(
26 | cmd, text=text, check=check, cwd=cwd, stderr=stderr, stdout=stdout, env=env
27 | )
28 |
--------------------------------------------------------------------------------
/tests/root.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import pytest
4 |
5 | TEST_ROOT = Path(__file__).parent.resolve()
6 | PROJECT_ROOT = TEST_ROOT.parent
7 |
8 |
9 | @pytest.fixture
10 | def test_root() -> Path:
11 | """
12 | Root directory of the tests
13 | """
14 | return TEST_ROOT
15 |
16 |
17 | @pytest.fixture
18 | def project_root() -> Path:
19 | """
20 | Root directory of the tests
21 | """
22 | return PROJECT_ROOT
23 |
--------------------------------------------------------------------------------
/tests/test_gc.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import subprocess
3 | import sys
4 | import unittest
5 |
6 | import pytest
7 |
8 | from .direnv_project import DirenvProject
9 | from .procs import run
10 |
11 | log = logging.getLogger(__name__)
12 |
13 |
14 | def common_test(direnv_project: DirenvProject) -> None:
15 | run(["nix-collect-garbage"])
16 |
17 | testenv = str(direnv_project.directory)
18 |
19 | out1 = run(
20 | ["direnv", "exec", testenv, "hello"],
21 | stderr=subprocess.PIPE,
22 | check=False,
23 | cwd=direnv_project.directory,
24 | )
25 | sys.stderr.write(out1.stderr)
26 | assert out1.returncode == 0
27 | assert "Renewed cache" in out1.stderr
28 | assert "Executing shellHook." in out1.stderr
29 |
30 | run(["nix-collect-garbage"])
31 |
32 | out2 = run(
33 | ["direnv", "exec", testenv, "hello"],
34 | stderr=subprocess.PIPE,
35 | check=False,
36 | cwd=direnv_project.directory,
37 | )
38 | sys.stderr.write(out2.stderr)
39 | assert out2.returncode == 0
40 | assert "Using cached dev shell" in out2.stderr
41 | assert "Executing shellHook." in out2.stderr
42 |
43 |
44 | def common_test_clean(direnv_project: DirenvProject) -> None:
45 | testenv = str(direnv_project.directory)
46 |
47 | out3 = run(
48 | ["direnv", "exec", testenv, "hello"],
49 | stderr=subprocess.PIPE,
50 | check=False,
51 | cwd=direnv_project.directory,
52 | )
53 | sys.stderr.write(out3.stderr)
54 |
55 | files = [
56 | path
57 | for path in (direnv_project.directory / ".direnv").iterdir()
58 | if path.is_file()
59 | ]
60 | rcs = [f for f in files if f.match("*.rc")]
61 | profiles = [f for f in files if not f.match("*.rc")]
62 | if len(rcs) != 1 or len(profiles) != 1:
63 | log.debug(files)
64 | assert len(rcs) == 1
65 | assert len(profiles) == 1
66 |
67 |
68 | @pytest.mark.parametrize("strict_env", [False, True])
69 | def test_use_nix(direnv_project: DirenvProject, strict_env: bool) -> None:
70 | direnv_project.setup_envrc("use nix", strict_env=strict_env)
71 | common_test(direnv_project)
72 |
73 | direnv_project.setup_envrc(
74 | "use nix --argstr shellHook 'echo Executing hijacked shellHook.'",
75 | strict_env=strict_env,
76 | )
77 | common_test_clean(direnv_project)
78 |
79 |
80 | @pytest.mark.parametrize("strict_env", [False, True])
81 | def test_use_flake(direnv_project: DirenvProject, strict_env: bool) -> None:
82 | direnv_project.setup_envrc("use flake", strict_env=strict_env)
83 | common_test(direnv_project)
84 | inputs = list((direnv_project.directory / ".direnv/flake-inputs").iterdir())
85 | # should only contain our flake-utils flake
86 | if len(inputs) != 4:
87 | run(["nix", "flake", "archive", "--json"], cwd=direnv_project.directory)
88 | log.debug(inputs)
89 | assert len(inputs) == 4
90 | for symlink in inputs:
91 | assert symlink.is_dir()
92 |
93 | direnv_project.setup_envrc("use flake --impure", strict_env=strict_env)
94 | common_test_clean(direnv_project)
95 |
96 |
97 | if __name__ == "__main__":
98 | unittest.main()
99 |
--------------------------------------------------------------------------------
/tests/test_use_nix.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import shlex
4 | import subprocess
5 | import sys
6 | import unittest
7 |
8 | import pytest
9 |
10 | from .direnv_project import DirenvProject
11 | from .procs import run
12 |
13 | log = logging.getLogger(__name__)
14 |
15 |
16 | def direnv_exec(
17 | direnv_project: DirenvProject, cmd: str, env: dict[str, str] | None = None
18 | ) -> None:
19 | args = ["direnv", "exec", str(direnv_project.directory), "sh", "-c", cmd]
20 | log.debug(f"$ {shlex.join(args)}")
21 | out = run(
22 | args,
23 | stderr=subprocess.PIPE,
24 | stdout=subprocess.PIPE,
25 | check=False,
26 | cwd=direnv_project.directory,
27 | env=env,
28 | )
29 | sys.stdout.write(out.stdout)
30 | sys.stderr.write(out.stderr)
31 | assert out.returncode == 0
32 | assert out.stdout == "OK\n"
33 | assert "Renewed cache" in out.stderr
34 |
35 |
36 | @pytest.mark.parametrize("strict_env", [False, True])
37 | def test_attrs(direnv_project: DirenvProject, strict_env: bool) -> None:
38 | direnv_project.setup_envrc("use nix -A subshell", strict_env=strict_env)
39 | direnv_exec(direnv_project, "echo $THIS_IS_A_SUBSHELL")
40 |
41 |
42 | @pytest.mark.parametrize("strict_env", [False, True])
43 | def test_no_nix_path(direnv_project: DirenvProject, strict_env: bool) -> None:
44 | direnv_project.setup_envrc("use nix --argstr someArg OK", strict_env=strict_env)
45 | env = os.environ.copy()
46 | del env["NIX_PATH"]
47 | direnv_exec(direnv_project, "echo $SHOULD_BE_SET", env=env)
48 |
49 |
50 | @pytest.mark.parametrize("strict_env", [False, True])
51 | def test_args(direnv_project: DirenvProject, strict_env: bool) -> None:
52 | direnv_project.setup_envrc("use nix --argstr someArg OK", strict_env=strict_env)
53 | direnv_exec(direnv_project, "echo $SHOULD_BE_SET")
54 |
55 |
56 | @pytest.mark.parametrize("strict_env", [False, True])
57 | def test_no_files(direnv_project: DirenvProject, strict_env: bool) -> None:
58 | direnv_project.setup_envrc("use nix -p hello", strict_env=strict_env)
59 | out = run(
60 | ["direnv", "status"],
61 | stderr=subprocess.PIPE,
62 | stdout=subprocess.PIPE,
63 | check=False,
64 | cwd=direnv_project.directory,
65 | )
66 | assert out.returncode == 0
67 | assert 'Loaded watch: "."' not in out.stdout
68 |
69 |
70 | if __name__ == "__main__":
71 | unittest.main()
72 |
--------------------------------------------------------------------------------
/tests/testenv/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-utils": {
4 | "inputs": {
5 | "systems": "systems"
6 | },
7 | "locked": {
8 | "lastModified": 1694529238,
9 | "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
10 | "owner": "numtide",
11 | "repo": "flake-utils",
12 | "rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
13 | "type": "github"
14 | },
15 | "original": {
16 | "owner": "numtide",
17 | "repo": "flake-utils",
18 | "type": "github"
19 | }
20 | },
21 | "nixpkgs": {
22 | "locked": {
23 | "lastModified": 1701401302,
24 | "narHash": "sha256-kfCOHzgtmHcgJwH7uagk8B+K1Qz58rN79eTLe55eGqA=",
25 | "owner": "NixOS",
26 | "repo": "nixpkgs",
27 | "rev": "69a165d0fd2b08a78dbd2c98f6f860ceb2bbcd40",
28 | "type": "github"
29 | },
30 | "original": {
31 | "owner": "NixOS",
32 | "ref": "nixpkgs-unstable",
33 | "repo": "nixpkgs",
34 | "type": "github"
35 | }
36 | },
37 | "root": {
38 | "inputs": {
39 | "flake-utils": "flake-utils",
40 | "nixpkgs": "nixpkgs"
41 | }
42 | },
43 | "systems": {
44 | "locked": {
45 | "lastModified": 1681028828,
46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
47 | "owner": "nix-systems",
48 | "repo": "default",
49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
50 | "type": "github"
51 | },
52 | "original": {
53 | "owner": "nix-systems",
54 | "repo": "default",
55 | "type": "github"
56 | }
57 | }
58 | },
59 | "root": "root",
60 | "version": 7
61 | }
62 |
--------------------------------------------------------------------------------
/tests/testenv/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "A very basic flake";
3 | inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
4 | inputs.flake-utils.url = "github:numtide/flake-utils";
5 |
6 | # deadnix: skip
7 | outputs =
8 | { nixpkgs, flake-utils, ... }:
9 | flake-utils.lib.eachDefaultSystem (system: {
10 | devShell = import ./shell.nix { pkgs = nixpkgs.legacyPackages.${system}; };
11 | });
12 | }
13 |
--------------------------------------------------------------------------------
/tests/testenv/shell.nix:
--------------------------------------------------------------------------------
1 | {
2 | pkgs ? import (builtins.getFlake (toString ./.)).inputs.nixpkgs { },
3 | someArg ? null,
4 | shellHook ? ''
5 | echo "Executing shellHook."
6 | '',
7 | }:
8 | pkgs.mkShellNoCC {
9 | inherit shellHook;
10 |
11 | nativeBuildInputs = [ pkgs.hello ];
12 | SHOULD_BE_SET = someArg;
13 |
14 | passthru = {
15 | subshell = pkgs.mkShellNoCC { THIS_IS_A_SUBSHELL = "OK"; };
16 | };
17 | }
18 |
--------------------------------------------------------------------------------
/treefmt.nix:
--------------------------------------------------------------------------------
1 | { inputs, ... }:
2 | {
3 | imports = [ inputs.treefmt-nix.flakeModule ];
4 |
5 | perSystem =
6 | { pkgs, ... }:
7 | {
8 | treefmt = {
9 | # Used to find the project root
10 | projectRootFile = ".git/config";
11 |
12 | programs = {
13 | deadnix.enable = true;
14 | deno.enable = true;
15 | mypy.enable = true;
16 | ruff.check = true;
17 | ruff.format = true;
18 | nixfmt.enable = true;
19 | nixfmt.package = pkgs.nixfmt-rfc-style;
20 | shellcheck.enable = true;
21 | shfmt.enable = true;
22 | statix.enable = true;
23 | yamlfmt.enable = true;
24 | };
25 |
26 | settings.formatter = {
27 | shellcheck.includes = [ "direnvrc" ];
28 | shfmt.includes = [ "direnvrc" ];
29 | };
30 | };
31 | };
32 | }
33 |
--------------------------------------------------------------------------------