├── .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 | ![Test](https://github.com/nix-community/nix-direnv/workflows/Test/badge.svg) 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 | --------------------------------------------------------------------------------