├── .gitattributes ├── .github └── workflows │ ├── pages.yml │ └── test.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE.MIT ├── UNLICENSE ├── build.rs ├── cached-nix-shell.1.md ├── default.nix ├── maintain.sh ├── nix-trace ├── .gitignore ├── Makefile ├── README.md ├── test.sh └── trace-nix.c ├── nix ├── Makefile ├── sources.json └── sources.nix ├── rcfile.sh ├── readme.md ├── rustfmt.toml ├── shell.nix ├── src ├── args.rs ├── bash.rs ├── drv.rs ├── main.rs ├── nix_path.rs ├── path_clean.rs ├── shebang.rs └── trace.rs └── tests ├── .gitignore ├── .shellcheckrc ├── lib.sh ├── run.sh ├── t01-lua.sh ├── t02-file-dep.sh ├── t03-path-pure-impure.sh ├── t04-env-pure-impure.sh ├── t05-attr.sh ├── t06-readdir.sh ├── t07-implicit-default-nix.sh ├── t08-args.sh ├── t09-I.sh ├── t10-non-utf8.sh ├── t11-wrap.sh ├── t12-shellhook-output.sh ├── t13-normalize-pwd.sh ├── t14-special-paths.sh ├── t15-old-nix.sh ├── t16-shopt.sh ├── t17-interactive.sh ├── t18-invalidate.sh └── t19-tarball-unpack.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | # Carnix 2 | Cargo.nix linguist-generated=true 3 | crates-io.nix linguist-generated=true 4 | 5 | # Niv 6 | nix/sources.nix linguist-generated=true 7 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | name: Pages 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - cached-nix-shell.1.md 8 | - Cargo.toml # Trigger the action on releases 9 | - .github/workflows/pages.yml 10 | jobs: 11 | pages: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - run: git fetch --prune --unshallow --tags 16 | - uses: cachix/install-nix-action@v27 17 | with: 18 | nix_path: nixpkgs=channel:nixos-unstable 19 | - run: nix-env -f '' -iA ronn 20 | - name: Build man page 21 | run: | 22 | ronn --organization="$(git describe --tags)" --style toc -5 cached-nix-shell.1.md 23 | mkdir pages 24 | mv ./cached-nix-shell.1.html pages 25 | - name: Deploy 26 | uses: peaceiris/actions-gh-pages@v4 27 | with: 28 | github_token: ${{ secrets.GITHUB_TOKEN }} 29 | publish_dir: ./pages 30 | user_name: github-actions[bot] 31 | user_email: github-actions[bot]@users.noreply.github.com 32 | commit_message: Regenerate man page 33 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | push: 5 | jobs: 6 | tests: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | os: [ubuntu-latest, macos-latest] 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: cachix/install-nix-action@v27 15 | with: 16 | nix_path: nixpkgs=channel:nixos-unstable 17 | - uses: cachix/cachix-action@v15 18 | with: 19 | name: xzfc 20 | signingKey: ${{ secrets.CACHIX_SIGNING_KEY }} 21 | - name: Build and install cached-nix-shell 22 | run: nix-env -i -f default.nix 23 | - name: Workaround for Darwin 24 | if: matrix.os == 'macos-latest' 25 | run: cached-nix-shell -p --run true 26 | - name: Test cached-nix-shell 27 | run: ./tests/run.sh 28 | - name: Test nix-trace 29 | run: nix-shell -p b3sum --run "nix-shell ./default.nix --run 'make -C ./nix-trace test'" 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.rs.bk 2 | /cached-nix-shell.1 3 | /result 4 | /target 5 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.22.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "arrayref" 22 | version = "0.3.7" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" 25 | 26 | [[package]] 27 | name = "arrayvec" 28 | version = "0.7.4" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" 31 | 32 | [[package]] 33 | name = "backtrace" 34 | version = "0.3.73" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" 37 | dependencies = [ 38 | "addr2line", 39 | "cc", 40 | "cfg-if", 41 | "libc", 42 | "miniz_oxide", 43 | "object", 44 | "rustc-demangle", 45 | ] 46 | 47 | [[package]] 48 | name = "bitflags" 49 | version = "2.5.0" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 52 | 53 | [[package]] 54 | name = "blake3" 55 | version = "1.5.1" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52" 58 | dependencies = [ 59 | "arrayref", 60 | "arrayvec", 61 | "cc", 62 | "cfg-if", 63 | "constant_time_eq", 64 | ] 65 | 66 | [[package]] 67 | name = "bytelines" 68 | version = "2.5.0" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "1297656b3c221f5251560da47ce530d981345d3dabe822067c18ecb36e67aacb" 71 | dependencies = [ 72 | "futures-util", 73 | "tokio", 74 | ] 75 | 76 | [[package]] 77 | name = "bytes" 78 | version = "1.6.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" 81 | 82 | [[package]] 83 | name = "cached-nix-shell" 84 | version = "0.1.6" 85 | dependencies = [ 86 | "blake3", 87 | "bytelines", 88 | "itertools", 89 | "nix", 90 | "nom", 91 | "once_cell", 92 | "serde_json", 93 | "tempfile", 94 | "ufcs", 95 | "which", 96 | "xdg", 97 | ] 98 | 99 | [[package]] 100 | name = "cc" 101 | version = "1.0.100" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "c891175c3fb232128f48de6590095e59198bbeb8620c310be349bfc3afd12c7b" 104 | 105 | [[package]] 106 | name = "cfg-if" 107 | version = "1.0.0" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 110 | 111 | [[package]] 112 | name = "cfg_aliases" 113 | version = "0.2.1" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 116 | 117 | [[package]] 118 | name = "constant_time_eq" 119 | version = "0.3.0" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" 122 | 123 | [[package]] 124 | name = "either" 125 | version = "1.12.0" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" 128 | 129 | [[package]] 130 | name = "errno" 131 | version = "0.3.9" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 134 | dependencies = [ 135 | "libc", 136 | "windows-sys", 137 | ] 138 | 139 | [[package]] 140 | name = "fastrand" 141 | version = "2.1.0" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" 144 | 145 | [[package]] 146 | name = "futures-core" 147 | version = "0.3.30" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 150 | 151 | [[package]] 152 | name = "futures-task" 153 | version = "0.3.30" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 156 | 157 | [[package]] 158 | name = "futures-util" 159 | version = "0.3.30" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 162 | dependencies = [ 163 | "futures-core", 164 | "futures-task", 165 | "pin-project-lite", 166 | "pin-utils", 167 | ] 168 | 169 | [[package]] 170 | name = "gimli" 171 | version = "0.29.0" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" 174 | 175 | [[package]] 176 | name = "home" 177 | version = "0.5.9" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" 180 | dependencies = [ 181 | "windows-sys", 182 | ] 183 | 184 | [[package]] 185 | name = "itertools" 186 | version = "0.13.0" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 189 | dependencies = [ 190 | "either", 191 | ] 192 | 193 | [[package]] 194 | name = "itoa" 195 | version = "1.0.11" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 198 | 199 | [[package]] 200 | name = "libc" 201 | version = "0.2.155" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 204 | 205 | [[package]] 206 | name = "linux-raw-sys" 207 | version = "0.4.14" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 210 | 211 | [[package]] 212 | name = "memchr" 213 | version = "2.7.4" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 216 | 217 | [[package]] 218 | name = "minimal-lexical" 219 | version = "0.2.1" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 222 | 223 | [[package]] 224 | name = "miniz_oxide" 225 | version = "0.7.4" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 228 | dependencies = [ 229 | "adler", 230 | ] 231 | 232 | [[package]] 233 | name = "nix" 234 | version = "0.29.0" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 237 | dependencies = [ 238 | "bitflags", 239 | "cfg-if", 240 | "cfg_aliases", 241 | "libc", 242 | ] 243 | 244 | [[package]] 245 | name = "nom" 246 | version = "7.1.3" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 249 | dependencies = [ 250 | "memchr", 251 | "minimal-lexical", 252 | ] 253 | 254 | [[package]] 255 | name = "object" 256 | version = "0.36.0" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" 259 | dependencies = [ 260 | "memchr", 261 | ] 262 | 263 | [[package]] 264 | name = "once_cell" 265 | version = "1.19.0" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 268 | 269 | [[package]] 270 | name = "pin-project-lite" 271 | version = "0.2.14" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 274 | 275 | [[package]] 276 | name = "pin-utils" 277 | version = "0.1.0" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 280 | 281 | [[package]] 282 | name = "proc-macro2" 283 | version = "1.0.86" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 286 | dependencies = [ 287 | "unicode-ident", 288 | ] 289 | 290 | [[package]] 291 | name = "quote" 292 | version = "1.0.36" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 295 | dependencies = [ 296 | "proc-macro2", 297 | ] 298 | 299 | [[package]] 300 | name = "rustc-demangle" 301 | version = "0.1.24" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 304 | 305 | [[package]] 306 | name = "rustix" 307 | version = "0.38.34" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" 310 | dependencies = [ 311 | "bitflags", 312 | "errno", 313 | "libc", 314 | "linux-raw-sys", 315 | "windows-sys", 316 | ] 317 | 318 | [[package]] 319 | name = "ryu" 320 | version = "1.0.18" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 323 | 324 | [[package]] 325 | name = "serde" 326 | version = "1.0.203" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" 329 | dependencies = [ 330 | "serde_derive", 331 | ] 332 | 333 | [[package]] 334 | name = "serde_derive" 335 | version = "1.0.203" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" 338 | dependencies = [ 339 | "proc-macro2", 340 | "quote", 341 | "syn", 342 | ] 343 | 344 | [[package]] 345 | name = "serde_json" 346 | version = "1.0.117" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" 349 | dependencies = [ 350 | "itoa", 351 | "ryu", 352 | "serde", 353 | ] 354 | 355 | [[package]] 356 | name = "syn" 357 | version = "2.0.67" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "ff8655ed1d86f3af4ee3fd3263786bc14245ad17c4c7e85ba7187fb3ae028c90" 360 | dependencies = [ 361 | "proc-macro2", 362 | "quote", 363 | "unicode-ident", 364 | ] 365 | 366 | [[package]] 367 | name = "tempfile" 368 | version = "3.10.1" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" 371 | dependencies = [ 372 | "cfg-if", 373 | "fastrand", 374 | "rustix", 375 | "windows-sys", 376 | ] 377 | 378 | [[package]] 379 | name = "tokio" 380 | version = "1.38.0" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" 383 | dependencies = [ 384 | "backtrace", 385 | "bytes", 386 | "pin-project-lite", 387 | ] 388 | 389 | [[package]] 390 | name = "ufcs" 391 | version = "0.1.0" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "b879826c277a7addb7b93bdd50d47c02710fe2845f4f07999887ca17fbc6b648" 394 | 395 | [[package]] 396 | name = "unicode-ident" 397 | version = "1.0.12" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 400 | 401 | [[package]] 402 | name = "which" 403 | version = "6.0.1" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7" 406 | dependencies = [ 407 | "either", 408 | "home", 409 | "rustix", 410 | "winsafe", 411 | ] 412 | 413 | [[package]] 414 | name = "windows-sys" 415 | version = "0.52.0" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 418 | dependencies = [ 419 | "windows-targets", 420 | ] 421 | 422 | [[package]] 423 | name = "windows-targets" 424 | version = "0.52.5" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 427 | dependencies = [ 428 | "windows_aarch64_gnullvm", 429 | "windows_aarch64_msvc", 430 | "windows_i686_gnu", 431 | "windows_i686_gnullvm", 432 | "windows_i686_msvc", 433 | "windows_x86_64_gnu", 434 | "windows_x86_64_gnullvm", 435 | "windows_x86_64_msvc", 436 | ] 437 | 438 | [[package]] 439 | name = "windows_aarch64_gnullvm" 440 | version = "0.52.5" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 443 | 444 | [[package]] 445 | name = "windows_aarch64_msvc" 446 | version = "0.52.5" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 449 | 450 | [[package]] 451 | name = "windows_i686_gnu" 452 | version = "0.52.5" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 455 | 456 | [[package]] 457 | name = "windows_i686_gnullvm" 458 | version = "0.52.5" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 461 | 462 | [[package]] 463 | name = "windows_i686_msvc" 464 | version = "0.52.5" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 467 | 468 | [[package]] 469 | name = "windows_x86_64_gnu" 470 | version = "0.52.5" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 473 | 474 | [[package]] 475 | name = "windows_x86_64_gnullvm" 476 | version = "0.52.5" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 479 | 480 | [[package]] 481 | name = "windows_x86_64_msvc" 482 | version = "0.52.5" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 485 | 486 | [[package]] 487 | name = "winsafe" 488 | version = "0.0.19" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" 491 | 492 | [[package]] 493 | name = "xdg" 494 | version = "2.5.2" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" 497 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cached-nix-shell" 3 | version = "0.1.6" 4 | authors = ["Albert Safin "] 5 | license = "Unlicense OR MIT" 6 | edition = "2018" 7 | 8 | [build-dependencies] 9 | which = "6.0.1" 10 | 11 | [dependencies] 12 | blake3 = "1.5.1" 13 | bytelines = "2.5.0" 14 | itertools = "0.13.0" 15 | nix = { version = "0.29.0", features = [ "fs" ] } 16 | nom = "7.1.3" 17 | once_cell = "1.19.0" 18 | serde_json = "1.0.117" 19 | tempfile = "3.10.1" 20 | ufcs = "0.1.0" 21 | xdg = "2.5.2" 22 | -------------------------------------------------------------------------------- /LICENSE.MIT: -------------------------------------------------------------------------------- 1 | Copyright 2019 Albert Safin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::env::{var, var_os}; 2 | use std::path::Path; 3 | use std::process::Command; 4 | 5 | fn main() { 6 | if var_os("CNS_IN_NIX_SHELL").is_none() { 7 | // Release build triggered by nix-build. Use paths relative to $out. 8 | let out = var("out").unwrap(); 9 | println!("cargo:rustc-env=CNS_TRACE_NIX_SO={out}/lib/trace-nix.so"); 10 | println!("cargo:rustc-env=CNS_VAR_EMPTY={out}/var/empty"); 11 | println!( 12 | "cargo:rustc-env=CNS_RCFILE={out}/share/cached-nix-shell/rcfile.sh" 13 | ); 14 | println!( 15 | "cargo:rustc-env=CNS_WRAP_PATH={out}/libexec/cached-nix-shell" 16 | ); 17 | 18 | // Use pinned nix and nix-shell binaries. 19 | println!( 20 | "cargo:rustc-env=CNS_NIX={}/", 21 | which::which("nix") 22 | .expect("command not found: nix") 23 | .parent() 24 | .unwrap() 25 | .as_os_str() 26 | .to_str() 27 | .unwrap() 28 | ); 29 | } else { 30 | // Developer build triggered by `nix-shell --run 'cargo build'`. 31 | // Use paths relative to the build directory. Additionally, place 32 | // trace-nix.so and a symlink to the build directory. 33 | let out_dir = var("OUT_DIR").unwrap(); 34 | let cmd = Command::new("make") 35 | .args([ 36 | "-C", 37 | "nix-trace", 38 | &format!("DESTDIR={out_dir}"), 39 | &format!("{out_dir}/trace-nix.so"), 40 | ]) 41 | .status() 42 | .unwrap(); 43 | assert!(cmd.success()); 44 | 45 | println!("cargo:rustc-env=CNS_TRACE_NIX_SO={out_dir}/trace-nix.so"); 46 | println!("cargo:rustc-env=CNS_VAR_EMPTY=/var/empty"); 47 | println!( 48 | "cargo:rustc-env=CNS_RCFILE={}/rcfile.sh", 49 | var("CARGO_MANIFEST_DIR").unwrap() 50 | ); 51 | 52 | if Path::new(&format!("{out_dir}/wrapper")).exists() { 53 | std::fs::remove_dir_all(format!("{out_dir}/wrapper")).unwrap(); 54 | } 55 | std::fs::create_dir_all(format!("{out_dir}/wrapper")).unwrap(); 56 | std::os::unix::fs::symlink( 57 | "../../../../cached-nix-shell", 58 | format!("{out_dir}/wrapper/nix-shell"), 59 | ) 60 | .unwrap(); 61 | println!("cargo:rustc-env=CNS_WRAP_PATH={out_dir}/wrapper"); 62 | 63 | // Use nix and nix-shell from $PATH at runtime. 64 | println!("cargo:rustc-env=CNS_NIX="); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /cached-nix-shell.1.md: -------------------------------------------------------------------------------- 1 | # cached-nix-shell - instant startup time for nix-shell(1) 2 | 3 | ## SYNOPSIS 4 | 5 | `cached-nix-shell` \[_options_]...
6 | `cached-nix-shell` _shebang-script_ \[_args_]...
7 | `cached-nix-shell --wrap` _cmd_ \[_args_]...
8 | 9 | ## DESCRIPTION 10 | 11 | `cached-nix-shell` is a caching layer for `nix-shell` featuring instant startup time on subsequent runs. 12 | The design goal is to make a fast drop-in replacement for `nix-shell`, including support of shebang scripts and non-interactive commands (i.e., `nix-shell --run ...`). 13 | 14 | ## OPTIONS 15 | 16 | `cached-nix-shell` supports the majority of `nix-shell` options, 17 | see the corresponding man page for the list. 18 | 19 | Additionally, the following new options are unique for `cached-nix-shell`: 20 | 21 | * `--exec` _cmd_ \[_args_]... (not in shebang): 22 | Command and arguments to be executed. 23 | It is similar to `--run` except that the command is executed directly rather than as shell command. 24 | It should be slightly faster and more convenient to pass arguments. 25 | 26 | * `--wrap` _cmd_ \[_args_]... (not in shebang, should be the first arg): 27 | Run the command substituting every invocation of `nix-shell` with `cached-nix-shell`. 28 | This is done by adding our symlink named `nix-shell` to the `$PATH`. 29 | 30 | ## ENVIRONMENT VARIABLES 31 | 32 | * `IN_CACHED_NIX_SHELL`: 33 | Is set to `1`. 34 | 35 | ## FILES 36 | 37 | The cache is stored in `$XDG_CACHE_HOME/cached-nix-shell`, 38 | defaults to `~/.cache/cached-nix-shell`. 39 | 40 | ## LIMITATIONS 41 | 42 | * Ambient environment variables: 43 | It is necessary to pass `--keep` _var_ even without `--pure` 44 | if the variable _var_ is used inside a nix expression or a hook. 45 | Note that updating the value of _var_ would invalidate the cache. 46 | 47 | * Relative paths: 48 | When `--expr` or `--packages` option is given, 49 | the cache is evaluated inside a separate empty directory, 50 | preventing access to relative paths from within nix expressions. 51 | Contrariwise, when a path to a shebang script or nix file is given, 52 | the cache is evaluated in the directory containing that script or file. 53 | This allows multiple `cached-nix-shell` invocations 54 | from different directories to reuse the same cache entry. 55 | 56 | * Network access: 57 | Accessing network resources (e.g. via `builtins.fetchurl`) is not considered in cache invalidation logic. 58 | Consequently, `tarball-ttl` option (see `nix-conf`(5)) is not respected. 59 | 60 | * Bash variables and functions: 61 | Only exported environment variables are preserved, 62 | but not global shell variables and functions set by `setup.sh`. 63 | 64 | * Shell hooks: 65 | Shell hooks are executed only once, during a cache evaluation. 66 | 67 | ## HOMEPAGE 68 | 69 | 70 | 71 | Please report bugs and feature requests in the issue tracker. 72 | 73 | ## SEE ALSO 74 | 75 | nix-shell(1) 76 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { }, nix ? pkgs.nix }: 2 | let 3 | sources = import ./nix/sources.nix; 4 | naersk = pkgs.callPackage sources.naersk { }; 5 | gitignoreSource = (pkgs.callPackage sources.gitignore { }).gitignoreSource; 6 | blake3-src = sources.BLAKE3; 7 | in (naersk.buildPackage { 8 | root = gitignoreSource ./.; 9 | buildInputs = [ pkgs.openssl nix pkgs.ronn ]; 10 | }).overrideAttrs (attrs: { 11 | CNS_GIT_COMMIT = if builtins.pathExists ./.git then 12 | pkgs.lib.commitIdFromGitRepo ./.git 13 | else 14 | "next"; 15 | BLAKE3_CSRC = "${blake3-src}/c"; 16 | postBuild = "make -f nix/Makefile post-build"; 17 | postInstall = "make -f nix/Makefile post-install"; 18 | }) 19 | -------------------------------------------------------------------------------- /maintain.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | trap rc=1 ERR; rc=0; case $1 in ################################################ 3 | ################################################################################ 4 | 5 | ''install) 6 | nix-env -i -f default.nix 7 | 8 | ;;build-nix) 9 | nix-build default.nix 10 | 11 | ;;format) 12 | nixfmt default.nix shell.nix 13 | cargo fmt 14 | 15 | ;;update) 16 | cargo upgrade 17 | cargo update 18 | niv update 19 | 20 | ;;lint) 21 | cd tests/ && shellcheck *.sh 22 | 23 | ;;test) 24 | cargo test 25 | ./tests/run.sh 26 | make -C ./nix-trace test 27 | 28 | ################################################################################ 29 | ;;*) cat $0; rc=1; esac; exit $rc ############################################## 30 | -------------------------------------------------------------------------------- /nix-trace/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | test-tmp 3 | -------------------------------------------------------------------------------- /nix-trace/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test clean 2 | 3 | DESTDIR := ./build 4 | 5 | BLAKE3_SRCS := blake3.c blake3_dispatch.c blake3_portable.c 6 | BLAKE3_FLAGS := -I $(BLAKE3_CSRC) 7 | ifneq ($(filter aarch64-%, $(system)),) 8 | BLAKE3_SRCS += blake3_neon.c 9 | else 10 | BLAKE3_FLAGS += -DBLAKE3_USE_NEON=0 11 | endif 12 | ifneq ($(filter x86_64-%, $(system)),) 13 | BLAKE3_SRCS += blake3_sse2_x86-64_unix.S \ 14 | blake3_sse41_x86-64_unix.S \ 15 | blake3_avx2_x86-64_unix.S \ 16 | blake3_avx512_x86-64_unix.S 17 | else 18 | BLAKE3_FLAGS += -DBLAKE3_NO_SSE2 -DBLAKE3_NO_SSE41 \ 19 | -DBLAKE3_NO_AVX2 -DBLAKE3_NO_AVX512 20 | endif 21 | 22 | $(DESTDIR)/trace-nix.so: trace-nix.c Makefile 23 | @mkdir -p $(DESTDIR) 24 | $(CC) -fPIC -shared -o $@ $< \ 25 | $(BLAKE3_FLAGS) $(addprefix $(BLAKE3_CSRC)/, $(BLAKE3_SRCS)) 26 | 27 | test: build/trace-nix.so 28 | ./test.sh 29 | 30 | clean: 31 | rm -rf build test-tmp 32 | -------------------------------------------------------------------------------- /nix-trace/README.md: -------------------------------------------------------------------------------- 1 | # trace-nix 2 | 3 | Using `LD_PRELOAD` trick to trace nix access to dirs and files. 4 | 5 | ## Usage 6 | 7 | Run `nix-shell` (or `nix repl` or any other nix tool) with the `TRACE_NIX` environment variable. 8 | 9 | Example: 10 | ``` bash 11 | LD_PRELOAD=/path/to/trace-nix.so TRACE_NIX=./log nix-shell -p stdenv --run : 12 | 13 | # The NUL-separated list of entries will be stored in `./log` 14 | ``` 15 | 16 | ## Log format 17 | 18 | Since the file names could contain arbitrary byte sequences (broken utf8, `\n`, etc), the NUL-separated format is chosen. 19 | 20 | ``` 21 | lstat() == -1: `s` FILENAME `\0` `-` `\0` 22 | lstat() == 0 && S_ISLNK(): `s` FILENAME `\0` `l` readlink(FILENAME) `\0` 23 | lstat() == 0 && S_ISDIR(): `s` FILENAME `\0` `d` `\0` 24 | lstat() == 0 && !S_ISDIR() && !S_ISLNK(): `s` FILENAME `\0` `+` `\0` 25 | 26 | open() == -1: `f` FILENAME `\0` `-` `\0` 27 | open() != -1: `f` FILENAME `\0` b3sum(file contents) `\0` 28 | open() != -1 && read error: `f` FILENAME `\0` `e` `\0` 29 | 30 | opendir() == NULL: `d` FILENAME `\0` `-` `\0` 31 | opendir() != NULL: `d` FILENAME `\0` b3sum(directory listing) `\0` 32 | 33 | mkdir() == 0 `t` FILENAME `\0` `+` `\0` 34 | unlinkat() == NULL `t` FILENAME `\0` `-` `\0` 35 | ``` 36 | 37 | Directory listing: 38 | ``` 39 | find -mindepth 1 -maxdepth 1 -printf '%P=%y\0' | sed -z 's/[^dlf]$/u/' | LC_ALL=C sort -z 40 | ``` 41 | -------------------------------------------------------------------------------- /nix-trace/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | run() { 4 | rm -f test-tmp/log 5 | env \ 6 | DYLD_INSERT_LIBRARIES="$PWD"/build/trace-nix.so \ 7 | LD_PRELOAD="$PWD"/build/trace-nix.so \ 8 | TRACE_NIX=test-tmp/log \ 9 | nix-shell --run : "$@" 2>/dev/null & 10 | NIX_PID=$! 11 | wait 12 | } 13 | 14 | result=0 15 | 16 | dir_b3sum() { 17 | find "$1" -mindepth 1 -maxdepth 1 -printf '%P=%y\0' | 18 | sed -z 's/[^dlf]$/u/' | 19 | LC_ALL=C sort -z | 20 | b3sum | 21 | head -c 32 22 | } 23 | 24 | check() { 25 | local grep_opts= 26 | [ "$1" != "--first" ] || { shift; grep_opts=-m1; } 27 | local name="$1" key="$2" val="$3" 28 | 29 | if ! grep -qzFx -- "$key" test-tmp/log; then 30 | printf "\33[31mFail: %s: can't find key\33[m\n" "$name" 31 | return 32 | result=1 33 | fi 34 | 35 | local actual_val=$( 36 | grep $grep_opts -zFx -A1 -- "$key" test-tmp/log | 37 | tail -zn1 | 38 | tr -d '\0' 39 | ) 40 | if [ "$val" != "$actual_val" ]; then 41 | printf "\33[31mFail: %s: expected '%s', got '%s'\33[m\n" \ 42 | "$name" "$val" "$actual_val" 43 | return 44 | result=1 45 | fi 46 | 47 | printf "\33[32mOK: %s\33[m\n" "$name" 48 | } 49 | 50 | rm -rf test-tmp 51 | mkdir test-tmp 52 | echo '"foo"' > test-tmp/test.nix 53 | : > test-tmp/empty 54 | ln -s empty test-tmp/link 55 | 56 | mkdir -p test-tmp/repo/data test-tmp/tmpdir 57 | echo '{}' > test-tmp/repo/data/default.nix 58 | tar -C test-tmp/repo -cf test-tmp/repo.tar . 59 | 60 | x="" 61 | for i in {1..64};do 62 | x=x$x 63 | mkdir -p test-tmp/many-dirs/$x 64 | done 65 | 66 | export XDG_CACHE_HOME="$PWD/test-tmp/xdg-cache" 67 | export TMPDIR="$PWD/test-tmp/tmpdir" 68 | 69 | 70 | run -p 'with import {}; bash' 71 | check import-channel \ 72 | "s/nix/var/nix/profiles/per-user/root/channels/unstable" \ 73 | "l$(readlink /nix/var/nix/profiles/per-user/root/channels/unstable)" 74 | 75 | run -p 'with import {}; bash' 76 | check import-channel-ne \ 77 | "s/nix/var/nix/profiles/per-user/root/channels/nonexistentChannel" '-' 78 | 79 | 80 | run -p 'import ./test-tmp/test.nix' 81 | check import-relative-nix \ 82 | "s$PWD/test-tmp/test.nix" "+" 83 | 84 | run -p 'import ./test-tmp' 85 | check import-relative-nix-dir \ 86 | "s$PWD/test-tmp" "d" 87 | 88 | run -p 'import ./nonexistent.nix' 89 | check import-relative-nix-ne \ 90 | "s$PWD/nonexistent.nix" "-" 91 | 92 | 93 | run -p 'builtins.readFile ./test-tmp/test.nix' 94 | check builtins.readFile \ 95 | "f$PWD/test-tmp/test.nix" \ 96 | "$(b3sum ./test-tmp/test.nix | head -c 32)" 97 | 98 | run -p 'builtins.readFile "/nonexistent/readFile"' 99 | check builtins.readFile-ne \ 100 | "f/nonexistent/readFile" "-" 101 | 102 | run -p 'builtins.readFile ./test-tmp' 103 | check builtins.readFile-dir \ 104 | "f$PWD/test-tmp" "e" 105 | 106 | run -p 'builtins.readFile ./test-tmp/empty' 107 | check builtins.readFile-empty \ 108 | "f$PWD/test-tmp/empty" \ 109 | "$(b3sum ./test-tmp/empty | head -c 32)" 110 | 111 | 112 | run -p 'builtins.readDir ./test-tmp' 113 | check builtins.readDir \ 114 | "d$PWD/test-tmp" "$(dir_b3sum ./test-tmp)" 115 | 116 | run -p 'builtins.readDir "/nonexistent/readDir"' 117 | check builtins.readDir-ne \ 118 | "d/nonexistent/readDir" "-" 119 | 120 | 121 | run -p 'builtins.readDir ./test-tmp/many-dirs' 122 | check builtins.readDir-many-dirs \ 123 | "d$PWD/test-tmp/many-dirs" "$(dir_b3sum ./test-tmp/many-dirs)" 124 | 125 | run 126 | check implicit:shell.nix \ 127 | "s$PWD/shell.nix" "-" 128 | check implicit:default.nix \ 129 | "s$PWD/default.nix" "-" 130 | 131 | 132 | run -p "fetchTarball file://$PWD/test-tmp/repo.tar?1" 133 | check --first fetchTarball:create \ 134 | "t$PWD/test-tmp/tmpdir/nix-$NIX_PID-1" "+" 135 | check fetchTarball:delete \ 136 | "t$PWD/test-tmp/tmpdir/nix-$NIX_PID-1" "-" 137 | 138 | run -I "q=file://$PWD/test-tmp/repo.tar?2" -p "import " 139 | check --first tarball-I:create \ 140 | "t$PWD/test-tmp/tmpdir/nix-$NIX_PID-1" "+" 141 | check tarball-I:delete \ 142 | "t$PWD/test-tmp/tmpdir/nix-$NIX_PID-1" "-" 143 | 144 | exit $result 145 | -------------------------------------------------------------------------------- /nix-trace/trace-nix.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | 3 | #include "blake3.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | static FILE *log_f = NULL; 20 | static const char *pwd = NULL; 21 | static char tmp_prefix[PATH_MAX]; // "$TMPDIR/nix-$$-" 22 | static size_t tmp_prefix_dirname_len = 0; // Length of "$TMPDIR" 23 | static size_t tmp_prefix_basename_len = 0; // Length of "nix-$$-" 24 | 25 | #define FATAL() \ 26 | do { \ 27 | fprintf(stderr, "nix-trace.c:%d: %s: %s\n", \ 28 | __LINE__, __func__, strerror(errno)); \ 29 | exit(2); \ 30 | } while(0) 31 | 32 | #define LEN 16 33 | 34 | // Locks 35 | 36 | #ifdef __APPLE__ 37 | 38 | #include 39 | static dispatch_semaphore_t print_mutex; 40 | static dispatch_semaphore_t buf_mutex; 41 | #define INIT_MUTEX(MUTEX) MUTEX = dispatch_semaphore_create(1) 42 | #define LOCK(MUTEX) dispatch_semaphore_wait(MUTEX, DISPATCH_TIME_FOREVER) 43 | #define UNLOCK(MUTEX) dispatch_semaphore_signal(MUTEX) 44 | 45 | #else 46 | 47 | #include 48 | static pthread_mutex_t print_mutex = PTHREAD_MUTEX_INITIALIZER; 49 | static pthread_mutex_t buf_mutex = PTHREAD_MUTEX_INITIALIZER; 50 | #define INIT_MUTEX(MUTEX) 51 | #define LOCK(MUTEX) pthread_mutex_lock(&MUTEX) 52 | #define UNLOCK(MUTEX) pthread_mutex_unlock(&MUTEX) 53 | 54 | #endif 55 | 56 | // Predeclarations 57 | 58 | static void convert_digest(char [static LEN*2+1], const uint8_t [static LEN]); 59 | static int enable(const char *); 60 | static void hash_dir(char [static LEN*2+1], DIR *); 61 | static void hash_file(char [static LEN*2+1], int); 62 | static void print_log(char, const char *, const char *); 63 | static void print_stat(int result, const char *path, struct stat *sb); 64 | static int strcmp_qsort(const void *, const void *); 65 | 66 | //////////////////////////////////////////////////////////////////////////////// 67 | 68 | static void __attribute__((constructor)) init() { 69 | // Remove ourselves from LD_PRELOAD and DYLD_INSERT_LIBRARIES. 70 | // We do not want to log child processes. 71 | // TODO: use `ld.so --preload` instead 72 | unsetenv("LD_PRELOAD"); 73 | unsetenv("DYLD_INSERT_LIBRARIES"); 74 | 75 | const char *fname = getenv("TRACE_NIX"); 76 | if (fname != NULL) { 77 | log_f = fopen(fname, "w"); 78 | if (log_f == NULL) { 79 | fprintf(stderr, "trace-nix: can't open file %s: %s\n", fname, 80 | strerror(errno)); 81 | errno = 0; 82 | } 83 | #ifdef __APPLE__ 84 | pwd = getcwd(NULL, 0); 85 | #else 86 | pwd = get_current_dir_name(); 87 | #endif 88 | if (pwd == NULL) 89 | FATAL(); 90 | } 91 | unsetenv("TRACE_NIX"); 92 | 93 | INIT_MUTEX(print_mutex); 94 | INIT_MUTEX(buf_mutex); 95 | 96 | // References: 97 | // https://github.com/NixOS/nix/blob/2.15.1/src/libutil/filesystem.cc#L18 98 | // https://github.com/NixOS/nix/blob/2.15.1/src/libutil/util.hh#L337-L338 99 | const char *tmpdir = getenv("TMPDIR"); 100 | if (tmpdir == NULL) 101 | tmpdir = "/tmp"; 102 | char tmpdir_real[PATH_MAX]; 103 | if (realpath(tmpdir, tmpdir_real) == NULL) { 104 | fprintf(stderr, "trace-nix: cannot resolve TMPDIR: %s\n", strerror(errno)); 105 | tmp_prefix[0] = '\0'; 106 | return; 107 | } 108 | const char *tmpdirend = tmpdir_real + strlen(tmpdir_real); 109 | while (tmpdirend > tmpdir_real && tmpdirend[-1] == '/') 110 | tmpdirend--; 111 | int len = snprintf(tmp_prefix, sizeof tmp_prefix, 112 | "%.*s/nix-%" PRIu64 "-", 113 | (int)(tmpdirend - tmpdir_real), 114 | tmpdir_real, 115 | (uint64_t)getpid()); 116 | tmp_prefix_dirname_len = tmpdirend - tmpdir_real; 117 | tmp_prefix_basename_len = len - tmp_prefix_dirname_len - 1; 118 | if (len < 0 || len >= sizeof tmp_prefix) { 119 | fprintf(stderr, "trace-nix: TMPDIR too long\n"); 120 | tmp_prefix[0] = '\0'; 121 | } 122 | } 123 | 124 | #ifdef __APPLE__ 125 | 126 | #define WRAPPER(RET, FUN, ARGS) \ 127 | static RET _cns_wrapper_##FUN ARGS; \ 128 | __attribute__((used)) static void *_cns_interpose_##FUN[2] \ 129 | __attribute__((section("__DATA,__interpose"))) = { &_cns_wrapper_##FUN, &FUN }; \ 130 | static RET _cns_wrapper_##FUN ARGS 131 | #define REAL(FUN) FUN 132 | 133 | #else 134 | 135 | #define WRAPPER(RET, FUN, ARGS) \ 136 | static RET (*_cns_real_##FUN)ARGS = NULL; \ 137 | RET FUN ARGS 138 | #define REAL(FUN) \ 139 | (_cns_real_##FUN == NULL ? (_cns_real_##FUN = dlsym(RTLD_NEXT, #FUN)) : _cns_real_##FUN) 140 | 141 | #endif 142 | 143 | WRAPPER(int, lstat, (const char *path, struct stat *sb)) { 144 | int result = REAL(lstat)(path, sb); 145 | print_stat(result, path, sb); 146 | return result; 147 | } 148 | 149 | #ifdef __linux__ 150 | WRAPPER(int, __lxstat, (int ver, const char *path, struct stat *sb)) { 151 | int result = REAL(__lxstat)(ver, path, sb); 152 | print_stat(result, path, sb); 153 | return result; 154 | } 155 | #endif 156 | 157 | WRAPPER(int, open, (const char *path, int flags, ...)) { 158 | va_list args; 159 | va_start(args, flags); 160 | int mode = va_arg(args, int); 161 | va_end(args); 162 | 163 | int fd = REAL(open)(path, flags, mode); 164 | 165 | if (flags == (O_RDONLY|O_CLOEXEC) && enable(path)) { 166 | if (fd == -1) { 167 | print_log('f', path, "-"); 168 | } else { 169 | char digest[LEN*2+1]; 170 | hash_file(digest, fd); 171 | print_log('f', path, digest); 172 | } 173 | } 174 | 175 | return fd; 176 | } 177 | 178 | WRAPPER(DIR *, opendir, (const char *path)) { 179 | DIR *dirp = REAL(opendir)(path); 180 | if (enable(path)) { 181 | if (dirp == NULL) { 182 | print_log('d', path, "-"); 183 | } else { 184 | char digest[LEN*2+1]; 185 | hash_dir(digest, dirp); 186 | print_log('d', path, digest); 187 | } 188 | } 189 | return dirp; 190 | } 191 | 192 | WRAPPER(int, mkdir, (const char *path, mode_t mode)) { 193 | int result = REAL(mkdir)(path, mode); 194 | if (result == 0 && *tmp_prefix && memcmp(path, tmp_prefix, 195 | tmp_prefix_dirname_len + 1 + tmp_prefix_basename_len) == 0) 196 | print_log('t', path, "+"); 197 | return result; 198 | } 199 | 200 | WRAPPER(int, unlinkat, (int dirfd, const char *path, int flags)) { 201 | int result = REAL(unlinkat)(dirfd, path, flags); 202 | if (result != 0 || *tmp_prefix == '\0' || flags != AT_REMOVEDIR) 203 | return result; 204 | size_t path_len = strlen(path); 205 | if (path_len > 45) // 45 == len(f"nix-{2**64}-{2**64}") 206 | return result; 207 | // Check that the path starts with 'nix-$$-' and do not contain slash. 208 | if (memcmp(path, tmp_prefix + tmp_prefix_dirname_len + 1, tmp_prefix_basename_len) != 0 || 209 | strchr(path + tmp_prefix_dirname_len + 1 + tmp_prefix_basename_len, '/')) 210 | return result; 211 | 212 | char dir_path[PATH_MAX]; 213 | #ifdef __linux__ 214 | snprintf(dir_path, sizeof dir_path, "/proc/self/fd/%d", dirfd); 215 | #elif defined(__APPLE__) 216 | if (fcntl(dirfd, F_GETPATH, dir_path) == -1) { 217 | fprintf(stderr, "trace-nix: fcntl(%d, F_GETPATH): %s\n", dirfd, strerror(errno)); 218 | return result; 219 | } 220 | #else 221 | #warning "Not implemented for this platform" 222 | return result; 223 | #endif 224 | char full_path[PATH_MAX]; 225 | if (realpath(dir_path, full_path) == NULL) { 226 | fprintf(stderr, "trace-nix: realpath(%s): %s\n", dir_path, strerror(errno)); 227 | return result; 228 | } 229 | 230 | size_t dir_path_len = strlen(full_path); 231 | if (dir_path_len + 1 + path_len + 1 > sizeof full_path) { 232 | fprintf(stderr, "trace-nix: path too long: %s/%s\n", full_path, path); 233 | return result; 234 | } 235 | full_path[dir_path_len] = '/'; 236 | memcpy(full_path + dir_path_len + 1, path, path_len + 1); 237 | 238 | print_log('t', full_path, "-"); 239 | return result; 240 | } 241 | 242 | //////////////////////////////////////////////////////////////////////////////// 243 | 244 | static int enable(const char *path) { 245 | if (log_f == NULL || (*path != '/' && strcmp(path, "shell.nix"))) 246 | return 0; 247 | 248 | static const char *ignored_paths[] = { 249 | "/dev/urandom", 250 | "/etc/ssl/certs/ca-certificates.crt", 251 | "/nix/var/nix/daemon-socket/socket", 252 | "/nix", 253 | "/nix/store", 254 | NULL, 255 | }; 256 | static const char *ignored_prefices[] = { 257 | "/nix/store/", // assuming store paths are immutable 258 | "/nix/var/nix/temproots/", 259 | "/proc/", 260 | NULL, 261 | }; 262 | for (const char **p = ignored_paths; *p; p++) 263 | if (!strcmp(path, *p)) 264 | return 0; 265 | for (const char **p = ignored_prefices; *p; p++) 266 | if (!memcmp(path, *p, strlen(*p))) 267 | return 0; 268 | 269 | return 1; 270 | } 271 | 272 | static void print_stat(int result, const char *path, struct stat *sb) { 273 | static char *buf = NULL; 274 | static off_t buf_len = 0; 275 | 276 | if (enable(path)) { 277 | if (result != 0) { 278 | print_log('s', path, "-"); 279 | } else if (S_ISLNK(sb->st_mode)) { 280 | LOCK(buf_mutex); 281 | if (buf_len < sb->st_size + 2) { 282 | buf_len = sb->st_size + 2; 283 | buf = realloc(buf, buf_len); 284 | if (buf == NULL) 285 | FATAL(); 286 | } 287 | ssize_t link_len = readlink(path, buf+1, sb->st_size); 288 | if (link_len < 0 || link_len != sb->st_size) 289 | FATAL(); 290 | buf[0] = 'l'; 291 | buf[sb->st_size+1] = 0; 292 | print_log('s', path, buf); 293 | UNLOCK(buf_mutex); 294 | } else if (S_ISDIR(sb->st_mode)) { 295 | print_log('s', path, "d"); 296 | } else { 297 | print_log('s', path, "+"); 298 | } 299 | } 300 | } 301 | 302 | static void print_log(char op, const char *path, const char *result) { 303 | LOCK(print_mutex); 304 | fprintf( 305 | log_f, 306 | "%c" "%s%s" "%s%c" "%s%c", 307 | op, 308 | path[0] == '/' ? "" : pwd, path[0] == '/' ? "" : "/", 309 | path, (char)0, 310 | result, (char)0 311 | ); 312 | fflush(log_f); 313 | UNLOCK(print_mutex); 314 | } 315 | 316 | static void hash_file(char digest_s[static LEN*2+1], int fd) { 317 | struct stat stat_; 318 | int rc = fstat(fd, &stat_); 319 | if (rc != 0) 320 | FATAL(); 321 | char *mmaped = NULL; 322 | if (stat_.st_size != 0) { 323 | mmaped = mmap(NULL, stat_.st_size, PROT_READ, MAP_PRIVATE, fd, 0); 324 | if (mmaped == MAP_FAILED) { 325 | strcpy(digest_s, "e"); 326 | return; 327 | } 328 | } 329 | 330 | blake3_hasher hasher; 331 | blake3_hasher_init(&hasher); 332 | blake3_hasher_update(&hasher, mmaped, stat_.st_size); 333 | uint8_t digest_b[LEN]; 334 | blake3_hasher_finalize(&hasher, digest_b, LEN); 335 | convert_digest(digest_s, digest_b); 336 | 337 | if (stat_.st_size != 0) { 338 | rc = munmap(mmaped, stat_.st_size); 339 | if (rc != 0) 340 | FATAL(); 341 | } 342 | } 343 | 344 | static int strcmp_qsort(const void *a, const void *b) { 345 | return strcmp(*(const char* const*)a, *(const char * const*)b); 346 | } 347 | 348 | static void hash_dir(char digest_s[static LEN*2+1], DIR *dirp) { 349 | // A dynamically growing array of strings 350 | size_t entries_total = 32, n = 0; 351 | char **entries = calloc(entries_total, sizeof(char*)); 352 | if (entries == NULL) 353 | FATAL(); 354 | 355 | struct dirent *ent; 356 | while ((ent = readdir(dirp))) { 357 | if (!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) 358 | continue; 359 | 360 | if (n+1 >= entries_total) { 361 | entries_total *= 2; 362 | entries = realloc(entries, entries_total * sizeof(char*)); 363 | if (entries == NULL) 364 | FATAL(); 365 | } 366 | 367 | char ent_type = 368 | ent->d_type == DT_DIR ? 'd' : 369 | ent->d_type == DT_LNK ? 'l' : 370 | ent->d_type == DT_REG ? 'f' : 371 | 'u'; 372 | int l = asprintf(&entries[n++], "%s=%c", ent->d_name, ent_type); 373 | if (l == -1) 374 | FATAL(); 375 | } 376 | 377 | qsort(entries, n, sizeof(char*), strcmp_qsort); 378 | 379 | // Calculate hash 380 | uint8_t digest_b[LEN]; 381 | blake3_hasher hasher; 382 | blake3_hasher_init(&hasher); 383 | for (size_t i = 0; i < n; i++) 384 | blake3_hasher_update(&hasher, entries[i], strlen(entries[i])+1); 385 | blake3_hasher_finalize(&hasher, digest_b, LEN); 386 | convert_digest(digest_s, digest_b); 387 | 388 | // Memory cleanup 389 | for (size_t i = 0; i < n; i++) 390 | free(entries[i]); 391 | free(entries); 392 | 393 | // Revert dirp into initial state 394 | rewinddir(dirp); 395 | } 396 | 397 | static void convert_digest(char digest_s[static LEN*2+1], const uint8_t digest_b[static LEN]) { 398 | for (int i = 0; i < LEN; i++) 399 | sprintf(digest_s + i*2, "%02x", (unsigned)digest_b[i]); 400 | } 401 | -------------------------------------------------------------------------------- /nix/Makefile: -------------------------------------------------------------------------------- 1 | no-default-target: ; @false 2 | 3 | VERSION := $(shell sed < Cargo.toml -n 's/^version *= *"\(.*\)".*/\1/p') 4 | 5 | post-build: 6 | make -C nix-trace 7 | ronn --organization="cached-nix-shell ${VERSION}" -r cached-nix-shell.1.md 8 | 9 | post-install: 10 | mkdir -p ${out}/lib 11 | cp nix-trace/build/trace-nix.so ${out}/lib 12 | 13 | mkdir -p ${out}/share/cached-nix-shell 14 | cp rcfile.sh ${out}/share/cached-nix-shell/ 15 | 16 | mkdir -p ${out}/share/man/man1 17 | cp cached-nix-shell.1 ${out}/share/man/man1/ 18 | 19 | mkdir -p ${out}/libexec/cached-nix-shell 20 | ln -s ${out}/bin/cached-nix-shell ${out}/libexec/cached-nix-shell/nix-shell 21 | 22 | mkdir -p ${out}/var/empty 23 | 24 | .PHONY: no-default-target post-build post-install 25 | -------------------------------------------------------------------------------- /nix/sources.json: -------------------------------------------------------------------------------- 1 | { 2 | "BLAKE3": { 3 | "branch": "1.5.1", 4 | "description": "official implementations of the BLAKE3 cryptographic hash function", 5 | "homepage": "", 6 | "owner": "BLAKE3-team", 7 | "repo": "BLAKE3", 8 | "rev": "54930c95227daaac4dcf1eb3028e2f4e0768d139", 9 | "sha256": "05v3174ajp193va9i8qh760zy5f30988pyi8bzvddbnak2f80da9", 10 | "type": "tarball", 11 | "url": "https://github.com/BLAKE3-team/BLAKE3/archive/54930c95227daaac4dcf1eb3028e2f4e0768d139.tar.gz", 12 | "url_template": "https://github.com///archive/.tar.gz" 13 | }, 14 | "gitignore": { 15 | "branch": "master", 16 | "description": "Nix function for filtering local git sources", 17 | "homepage": "", 18 | "owner": "hercules-ci", 19 | "repo": "gitignore", 20 | "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 21 | "sha256": "02wxkdpbhlm3yk5mhkhsp3kwakc16xpmsf2baw57nz1dg459qv8w", 22 | "type": "tarball", 23 | "url": "https://github.com/hercules-ci/gitignore/archive/637db329424fd7e46cf4185293b9cc8c88c95394.tar.gz", 24 | "url_template": "https://github.com///archive/.tar.gz" 25 | }, 26 | "naersk": { 27 | "branch": "master", 28 | "description": "Build rust crates in Nix. No configuration, no code generation. IFD and sandbox friendly.", 29 | "homepage": "", 30 | "owner": "nmattia", 31 | "repo": "naersk", 32 | "rev": "941ce6dc38762a7cfb90b5add223d584feed299b", 33 | "sha256": "0y9hnqs3v09zrcia11f18ynx9c52h4a6mc26nwlrbnh8cv0h4nxq", 34 | "type": "tarball", 35 | "url": "https://github.com/nmattia/naersk/archive/941ce6dc38762a7cfb90b5add223d584feed299b.tar.gz", 36 | "url_template": "https://github.com///archive/.tar.gz" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /nix/sources.nix: -------------------------------------------------------------------------------- 1 | # This file has been generated by Niv. 2 | 3 | let 4 | 5 | # 6 | # The fetchers. fetch_ fetches specs of type . 7 | # 8 | 9 | fetch_file = pkgs: name: spec: 10 | let 11 | name' = sanitizeName name + "-src"; 12 | in 13 | if spec.builtin or true then 14 | builtins_fetchurl { inherit (spec) url sha256; name = name'; } 15 | else 16 | pkgs.fetchurl { inherit (spec) url sha256; name = name'; }; 17 | 18 | fetch_tarball = pkgs: name: spec: 19 | let 20 | name' = sanitizeName name + "-src"; 21 | in 22 | if spec.builtin or true then 23 | builtins_fetchTarball { name = name'; inherit (spec) url sha256; } 24 | else 25 | pkgs.fetchzip { name = name'; inherit (spec) url sha256; }; 26 | 27 | fetch_git = name: spec: 28 | let 29 | ref = 30 | spec.ref or ( 31 | if spec ? branch then "refs/heads/${spec.branch}" else 32 | if spec ? tag then "refs/tags/${spec.tag}" else 33 | abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!" 34 | ); 35 | submodules = spec.submodules or false; 36 | submoduleArg = 37 | let 38 | nixSupportsSubmodules = builtins.compareVersions builtins.nixVersion "2.4" >= 0; 39 | emptyArgWithWarning = 40 | if submodules 41 | then 42 | builtins.trace 43 | ( 44 | "The niv input \"${name}\" uses submodules " 45 | + "but your nix's (${builtins.nixVersion}) builtins.fetchGit " 46 | + "does not support them" 47 | ) 48 | { } 49 | else { }; 50 | in 51 | if nixSupportsSubmodules 52 | then { inherit submodules; } 53 | else emptyArgWithWarning; 54 | in 55 | builtins.fetchGit 56 | ({ url = spec.repo; inherit (spec) rev; inherit ref; } // submoduleArg); 57 | 58 | fetch_local = spec: spec.path; 59 | 60 | fetch_builtin-tarball = name: throw 61 | ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`. 62 | $ niv modify ${name} -a type=tarball -a builtin=true''; 63 | 64 | fetch_builtin-url = name: throw 65 | ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`. 66 | $ niv modify ${name} -a type=file -a builtin=true''; 67 | 68 | # 69 | # Various helpers 70 | # 71 | 72 | # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695 73 | sanitizeName = name: 74 | ( 75 | concatMapStrings (s: if builtins.isList s then "-" else s) 76 | ( 77 | builtins.split "[^[:alnum:]+._?=-]+" 78 | ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name) 79 | ) 80 | ); 81 | 82 | # The set of packages used when specs are fetched using non-builtins. 83 | mkPkgs = sources: system: 84 | let 85 | sourcesNixpkgs = 86 | import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; }; 87 | hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; 88 | hasThisAsNixpkgsPath = == ./.; 89 | in 90 | if builtins.hasAttr "nixpkgs" sources 91 | then sourcesNixpkgs 92 | else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then 93 | import { } 94 | else 95 | abort 96 | '' 97 | Please specify either (through -I or NIX_PATH=nixpkgs=...) or 98 | add a package called "nixpkgs" to your sources.json. 99 | ''; 100 | 101 | # The actual fetching function. 102 | fetch = pkgs: name: spec: 103 | 104 | if ! builtins.hasAttr "type" spec then 105 | abort "ERROR: niv spec ${name} does not have a 'type' attribute" 106 | else if spec.type == "file" then fetch_file pkgs name spec 107 | else if spec.type == "tarball" then fetch_tarball pkgs name spec 108 | else if spec.type == "git" then fetch_git name spec 109 | else if spec.type == "local" then fetch_local spec 110 | else if spec.type == "builtin-tarball" then fetch_builtin-tarball name 111 | else if spec.type == "builtin-url" then fetch_builtin-url name 112 | else 113 | abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; 114 | 115 | # If the environment variable NIV_OVERRIDE_${name} is set, then use 116 | # the path directly as opposed to the fetched source. 117 | replace = name: drv: 118 | let 119 | saneName = stringAsChars (c: if (builtins.match "[a-zA-Z0-9]" c) == null then "_" else c) name; 120 | ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}"; 121 | in 122 | if ersatz == "" then drv else 123 | # this turns the string into an actual Nix path (for both absolute and 124 | # relative paths) 125 | if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}"; 126 | 127 | # Ports of functions for older nix versions 128 | 129 | # a Nix version of mapAttrs if the built-in doesn't exist 130 | mapAttrs = builtins.mapAttrs or ( 131 | f: set: with builtins; 132 | listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)) 133 | ); 134 | 135 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 136 | range = first: last: if first > last then [ ] else builtins.genList (n: first + n) (last - first + 1); 137 | 138 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 139 | stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); 140 | 141 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 142 | stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); 143 | concatMapStrings = f: list: concatStrings (map f list); 144 | concatStrings = builtins.concatStringsSep ""; 145 | 146 | # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331 147 | optionalAttrs = cond: as: if cond then as else { }; 148 | 149 | # fetchTarball version that is compatible between all the versions of Nix 150 | builtins_fetchTarball = { url, name ? null, sha256 }@attrs: 151 | let 152 | inherit (builtins) lessThan nixVersion fetchTarball; 153 | in 154 | if lessThan nixVersion "1.12" then 155 | fetchTarball ({ inherit url; } // (optionalAttrs (name != null) { inherit name; })) 156 | else 157 | fetchTarball attrs; 158 | 159 | # fetchurl version that is compatible between all the versions of Nix 160 | builtins_fetchurl = { url, name ? null, sha256 }@attrs: 161 | let 162 | inherit (builtins) lessThan nixVersion fetchurl; 163 | in 164 | if lessThan nixVersion "1.12" then 165 | fetchurl ({ inherit url; } // (optionalAttrs (name != null) { inherit name; })) 166 | else 167 | fetchurl attrs; 168 | 169 | # Create the final "sources" from the config 170 | mkSources = config: 171 | mapAttrs 172 | ( 173 | name: spec: 174 | if builtins.hasAttr "outPath" spec 175 | then 176 | abort 177 | "The values in sources.json should not have an 'outPath' attribute" 178 | else 179 | spec // { outPath = replace name (fetch config.pkgs name spec); } 180 | ) 181 | config.sources; 182 | 183 | # The "config" used by the fetchers 184 | mkConfig = 185 | { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null 186 | , sources ? if sourcesFile == null then { } else builtins.fromJSON (builtins.readFile sourcesFile) 187 | , system ? builtins.currentSystem 188 | , pkgs ? mkPkgs sources system 189 | }: rec { 190 | # The sources, i.e. the attribute set of spec name to spec 191 | inherit sources; 192 | 193 | # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers 194 | inherit pkgs; 195 | }; 196 | 197 | in 198 | mkSources (mkConfig { }) // { __functor = _: settings: mkSources (mkConfig settings); } 199 | -------------------------------------------------------------------------------- /rcfile.sh: -------------------------------------------------------------------------------- 1 | [ "$IN_NIX_SHELL" = impure ] && [ -n "$PS1" ] && [ -e ~/.bashrc ] && source ~/.bashrc 2 | [ -n "$PS1" -a -z "$NIX_SHELL_PRESERVE_PROMPT" ] && PS1='\n\[\033[1;32m\][cached-nix-shell:\w]\$\[\033[0m\] ' 3 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # cached-nix-shell 2 | [![Build Status](https://img.shields.io/github/actions/workflow/status/xzfc/cached-nix-shell/test.yml?branch=master&logo=github)](https://github.com/xzfc/cached-nix-shell/actions/workflows/test.yml?query=branch%3Amaster) 3 | ![License](https://img.shields.io/badge/license-Unlicense%20OR%20MIT-blue) 4 | [![Nixpkgs unstable package](https://repology.org/badge/version-for-repo/nix_unstable/cached-nix-shell.svg)](https://nixos.org/nixos/packages.html?attr=cached-nix-shell&channel=nixpkgs-unstable&query=cached-nix-shell) 5 | [![Man page](https://img.shields.io/badge/man-cached--nix--shell%281%29-blue)](https://xzfc.github.io/cached-nix-shell/cached-nix-shell.1) 6 | 7 | `cached-nix-shell` is a caching layer for `nix-shell` featuring instant startup time on subsequent runs. 8 | 9 | It supports NixOS and Linux. 10 | 11 | ## Installation 12 | 13 | Install the release version from Nixpkgs: 14 | ```sh 15 | nix-env -iA nixpkgs.cached-nix-shell 16 | ``` 17 | 18 | Or, install the latest development version from GitHub: 19 | ```sh 20 | nix-env -if https://github.com/xzfc/cached-nix-shell/tarball/master 21 | ``` 22 | 23 | ## Usage 24 | 25 | Just replace `nix-shell` with `cached-nix-shell` in the shebang line: 26 | 27 | ```python 28 | #! /usr/bin/env cached-nix-shell 29 | #! nix-shell -i python3 -p python 30 | print("Hello, World!") 31 | ``` 32 | 33 | Alternatively, call `cached-nix-shell` directly: 34 | 35 | ```sh 36 | $ cached-nix-shell ./hello.py 37 | $ cached-nix-shell -p python3 --run 'python --version' 38 | ``` 39 | 40 | Or use the `--wrap` option for programs that call `nix-shell` internally. 41 | 42 | ```sh 43 | $ cached-nix-shell --wrap stack build 44 | ``` 45 | 46 | ## Performance 47 | 48 | ``` 49 | $ time ./hello.py # first run; no cache used 50 | cached-nix-shell: updating cache 51 | Hello, World! 52 | ./hello.py 0.33s user 0.06s system 91% cpu 0.435 total 53 | $ time ./hello.py 54 | Hello, World! 55 | ./hello.py 0.02s user 0.01s system 97% cpu 0.029 total 56 | ``` 57 | 58 | ## Caching and cache invalidation 59 | 60 | `cached-nix-shell` stores environment variables set up by `nix-shell` and reuses them on subsequent runs. 61 | It [traces](./nix-trace) which files are read by `nix` during an evaluation, and performs a proper cache invalidation if any of the used files are changed. 62 | The cache is stored in `~/.cache/cached-nix-shell/`. 63 | 64 | The following situations are covered: 65 | 66 | * `builtins.readFile` is used 67 | * `builtins.readDir` is used 68 | * `import ./file.nix` is used 69 | * updating `/etc/nix/nix.conf` or `~/.config/nix/nix.conf` 70 | * updating nix channels 71 | * updating `$NIX_PATH` environment variable 72 | 73 | The following situations aren't handled by `cached-nix-shell` and may lead to staled cache: 74 | 75 | * `builtins.fetchurl` or other network builtins are used (e.g. in [nixpkgs-mozilla]) 76 | 77 | [nixpkgs-mozilla]: https://github.com/mozilla/nixpkgs-mozilla 78 | 79 | ## Related 80 | 81 | * https://discourse.nixos.org/t/speeding-up-nix-shell-shebang/4048 82 | * There are related projects focused on using `nix-shell` for project developing: 83 | * [direnv](https://direnv.net/) with [use_nix](https://github.com/direnv/direnv/wiki/Nix) 84 | * [Cached and Persistent Nix shell with direnv integration](https://gist.github.com/mbbx6spp/731076cb8fc620b064b8e5b28fb1c796) 85 | * [lorri](https://github.com/nix-community/lorri), a `nix-shell` replacement for project development 86 | * [lorri #167](https://github.com/target/lorri/issues/167) 87 | * [nix-develop](https://gitlab.com/mightybyte/nix-develop), universal build tool featuring cached `nd shell` command 88 | 89 | * [https://github.com/rycee/home-manager/issues/447](https://github.com/rycee/home-manager/issues/447) 90 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { } }: 2 | let main = import ./default.nix { inherit pkgs; }; 3 | in with pkgs; 4 | mkShell { 5 | buildInputs = main.buildInputs ++ main.nativeBuildInputs ++ [ 6 | cargo-edit 7 | clippy 8 | niv 9 | nixfmt 10 | rust-analyzer 11 | rustc 12 | rustfmt 13 | shellcheck 14 | # Required for cargo 15 | git 16 | openssh 17 | ]; 18 | inherit (main) BLAKE3_CSRC; 19 | CNS_IN_NIX_SHELL = "1"; 20 | RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; 21 | } 22 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | //! nix-shell argument parsing 2 | //! 3 | //! While `cached-nix-shell` passes most of its arguments to `nix-shell` as-is 4 | //! without even looking into them, there are some arguments that should be 5 | //! extracted and processed by `cached-nix-shell` itself. In order to do so, we 6 | //! still need to parse the whole command line. 7 | //! 8 | //! **Q:** Why not just use a library like `clap` or `docopts`? 9 | //! **A:** We need to emulate quirks of nix-shell argument parsing in a 100% 10 | //! compatible way, so it is appropriate to code this explicitly rather than use 11 | //! such libraries. 12 | 13 | use std::collections::VecDeque; 14 | use std::ffi::{OsStr, OsString}; 15 | use std::io::Write; 16 | use std::os::unix::ffi::OsStrExt; 17 | use std::os::unix::process::CommandExt; 18 | use std::process::{exit, Command}; 19 | use ufcs::Pipe; 20 | 21 | pub enum RunMode { 22 | /// no arg 23 | InteractiveShell, 24 | /// --run CMD | --command CMD 25 | Shell(OsString), 26 | /// --exec CMD ARGS... 27 | Exec(OsString, Vec), 28 | } 29 | 30 | pub struct Args { 31 | /// true: -p | --packages | -E | --expr 32 | pub packages_or_expr: bool, 33 | /// true: --pure; false: --impure 34 | pub pure: bool, 35 | /// -I 36 | pub include_nix_path: Vec, 37 | /// -i (in shebang) 38 | pub interpreter: OsString, 39 | /// --run | --command | --exec (not in shebang) 40 | pub run: RunMode, 41 | /// --keep 42 | pub keep: Vec, 43 | /// other positional arguments (after --) 44 | pub rest: Vec, 45 | /// other keyword arguments 46 | pub other_kw: Vec, 47 | /// weak keyword arguments 48 | pub weak_kw: Vec, 49 | } 50 | 51 | struct NixShellOption { 52 | /// true if adding or removing this option should not invalidate the cache 53 | is_weak: bool, 54 | arg_count: u8, 55 | names: &'static [&'static str], 56 | } 57 | 58 | const fn opt( 59 | is_weak: bool, 60 | arg_count: u8, 61 | names: &'static [&'static str], 62 | ) -> NixShellOption { 63 | NixShellOption { 64 | is_weak, 65 | arg_count, 66 | names, 67 | } 68 | } 69 | 70 | const OPTIONS_DB: &[NixShellOption] = &[ 71 | opt(false, 1, &["--attr", "-A"]), 72 | opt(false, 2, &["--arg"]), 73 | opt(false, 2, &["--argstr"]), 74 | opt(true, 0, &["--fallback"]), 75 | opt(true, 0, &["--keep-failed", "-K"]), 76 | opt(true, 0, &["--keep-going", "-k"]), 77 | opt(true, 0, &["--no-build-hook"]), 78 | opt(true, 0, &["--no-build-output", "-Q"]), 79 | opt(true, 0, &["--quiet"]), 80 | opt(true, 0, &["--repair"]), 81 | opt(true, 0, &["--show-trace"]), 82 | opt(true, 0, &["--verbose", "-v"]), 83 | opt(true, 1, &["--cores"]), 84 | opt(true, 1, &["--max-jobs", "-j"]), 85 | opt(true, 1, &["--max-silent-time"]), 86 | opt(true, 1, &["--timeout"]), 87 | opt(true, 2, &["--option"]), 88 | ]; 89 | 90 | impl Args { 91 | pub fn parse( 92 | args: Vec, 93 | in_shebang: bool, 94 | ) -> Result { 95 | let mut res = Args { 96 | packages_or_expr: false, 97 | pure: false, 98 | include_nix_path: Vec::new(), 99 | interpreter: OsString::from("bash"), 100 | run: RunMode::InteractiveShell, 101 | keep: Vec::new(), 102 | rest: Vec::new(), 103 | other_kw: Vec::new(), 104 | weak_kw: Vec::new(), 105 | }; 106 | let mut it = VecDeque::::from(args); 107 | while let Some(arg) = get_next_arg(&mut it) { 108 | let mut next = || -> Result { 109 | it.pop_front() 110 | .ok_or_else(|| { 111 | format!("flag {arg:?} requires more arguments") 112 | })? 113 | .pipe(Ok) 114 | }; 115 | if let Some(db_item) = OPTIONS_DB 116 | .iter() 117 | .find(|it| it.names.iter().any(|&x| arg == x)) 118 | { 119 | let vec = if db_item.is_weak { 120 | &mut res.weak_kw 121 | } else { 122 | &mut res.other_kw 123 | }; 124 | vec.push(db_item.names[0].into()); 125 | for _ in 0..db_item.arg_count { 126 | vec.push(next()?); 127 | } 128 | } else if arg == "--pure" { 129 | res.pure = true; 130 | } else if arg == "--impure" { 131 | res.pure = false; 132 | } else if arg == "-I" { 133 | let path = next()?; 134 | res.include_nix_path.push(path.clone()); 135 | res.other_kw.push(arg); 136 | res.other_kw.push(path); 137 | } else if arg == "-p" 138 | || arg == "--packages" 139 | || arg == "-E" 140 | || arg == "--expr" 141 | { 142 | res.packages_or_expr = true; 143 | res.other_kw.push(arg); 144 | } else if arg == "-i" && in_shebang { 145 | res.interpreter = next()?; 146 | } else if (arg == "--run" || arg == "--command") && !in_shebang { 147 | res.run = RunMode::Shell(next()?); 148 | } else if arg == "--exec" && !in_shebang { 149 | res.run = RunMode::Exec(next()?, it.into()); 150 | break; 151 | } else if arg == "--keep" { 152 | res.keep.push(next()?); 153 | } else if arg == "--version" { 154 | exit_version(); 155 | } else if arg == "--wrap" && !in_shebang { 156 | return Err("--wrap should be the first argument".to_string()); 157 | } else if arg.as_bytes().first() == Some(&b'-') { 158 | return Err(format!("unexpected arg {arg:?}")); 159 | } else { 160 | res.rest.push(arg.clone()); 161 | } 162 | } 163 | Ok(res) 164 | } 165 | } 166 | 167 | fn get_next_arg(it: &mut VecDeque) -> Option { 168 | let arg = it.pop_front()?; 169 | let argb = arg.as_bytes(); 170 | if argb.len() > 2 && argb[0] == b'-' && is_alpha(argb[1]) { 171 | // Expand short options and put them back to the deque. 172 | // Reference: https://github.com/NixOS/nix/blob/2.3.1/src/libutil/args.cc#L29-L42 173 | 174 | let split_idx = argb[1..] 175 | .iter() 176 | .position(|&b| !is_alpha(b)) 177 | .unwrap_or(argb.len() - 1); 178 | // E.g. "-pj16" -> ("pj", "16") 179 | let (letters, rest) = argb[1..].split_at(split_idx); 180 | 181 | if !rest.is_empty() { 182 | it.push_front(OsStr::from_bytes(rest).into()); 183 | } 184 | for &c in letters.iter().rev() { 185 | it.push_front(OsStr::from_bytes(&[b'-', c]).into()); 186 | } 187 | 188 | it.pop_front() 189 | } else { 190 | Some(arg) 191 | } 192 | } 193 | 194 | fn is_alpha(b: u8) -> bool { 195 | b.is_ascii_lowercase() || b.is_ascii_uppercase() 196 | } 197 | 198 | fn exit_version() { 199 | println!( 200 | "cached-nix-shell v{}{}", 201 | env!("CARGO_PKG_VERSION"), 202 | option_env!("CNS_GIT_COMMIT") 203 | .map(|x| format!("-{x}")) 204 | .unwrap_or("".into()) 205 | ); 206 | if env!("CNS_NIX").is_empty() { 207 | println!("Using nix-shell from $PATH"); 208 | } else { 209 | println!("Using {}nix-shell", env!("CNS_NIX")); 210 | } 211 | std::io::stdout().flush().unwrap(); 212 | Command::new(concat!(env!("CNS_NIX"), "nix-shell")) 213 | .arg("--version") 214 | .exec(); 215 | exit(1); 216 | } 217 | 218 | #[cfg(test)] 219 | mod test { 220 | use super::*; 221 | /// Expand an arg using `get_next_arg` 222 | fn expand(arg: &str) -> Vec { 223 | let mut it: VecDeque = VecDeque::from(vec![arg.into()]); 224 | std::iter::from_fn(|| get_next_arg(&mut it)) 225 | .map(|s| s.to_string_lossy().into()) 226 | .collect() 227 | } 228 | #[test] 229 | fn test_get_next_arg() { 230 | assert_eq!(expand("--"), vec!["--"]); 231 | assert_eq!(expand("default.nix"), vec!["default.nix"]); 232 | assert_eq!(expand("--argstr"), vec!["--argstr"]); 233 | assert_eq!(expand("-pi"), vec!["-p", "-i"]); 234 | assert_eq!(expand("-j4"), vec!["-j", "4"]); 235 | assert_eq!(expand("-j16"), vec!["-j", "16"]); 236 | assert_eq!(expand("-pj16"), vec!["-p", "-j", "16"]); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/bash.rs: -------------------------------------------------------------------------------- 1 | pub fn is_literal_bash_string(command: &[u8]) -> bool { 2 | let mut previous = None; 3 | for &c in command { 4 | if b"\t\n !\"$&'()*,;<>?[\\]^`{|}".contains(&c) { 5 | return false; 6 | } 7 | if previous.is_none() && b"#-~".contains(&c) { 8 | // Special case: `-` isn't a part of bash syntax, but it is treated 9 | // as an argument of `exec`. 10 | return false; 11 | } 12 | if (previous == Some(b':') || previous == Some(b'=')) && c == b'~' { 13 | return false; 14 | } 15 | previous = Some(c); 16 | } 17 | true 18 | } 19 | 20 | pub fn quote(arg: &[u8]) -> Vec { 21 | let mut result = vec![b'\'']; 22 | for &i in arg { 23 | if i == b'\'' { 24 | result.extend_from_slice(br#"'\''"#) 25 | } else { 26 | result.push(i) 27 | } 28 | } 29 | result.push(b'\''); 30 | result 31 | } 32 | -------------------------------------------------------------------------------- /src/drv.rs: -------------------------------------------------------------------------------- 1 | use nom::branch::alt; 2 | use nom::bytes::complete::{escaped_transform, is_not, tag}; 3 | use nom::character::complete::char; 4 | use nom::combinator::{eof, opt, value}; 5 | use nom::multi::separated_list0; 6 | use nom::sequence::{delimited, preceded, tuple}; 7 | use nom::{AsChar, IResult, InputIter, Slice}; 8 | use std::ffi::{OsStr, OsString}; 9 | use std::fmt::Debug; 10 | use std::fs::read; 11 | use std::ops::RangeFrom; 12 | use std::os::unix::prelude::OsStringExt; 13 | use std::path::Path; 14 | 15 | #[derive(Debug)] 16 | #[allow(dead_code)] 17 | struct Derivation { 18 | outputs: Vec<(OsString, OsString, OsString, OsString)>, 19 | input_drvs: Vec<(OsString, Vec)>, 20 | input_srcs: Vec, 21 | platform: OsString, 22 | builder: OsString, 23 | args: Vec, 24 | env: Vec<(OsString, OsString)>, 25 | } 26 | 27 | /// Check if .drv file is present, and all of its inputs (both .drv and their 28 | /// outputs) are present. 29 | pub fn derivation_is_ok>(path: P) -> Result<(), String> { 30 | // nix-shell doesn't create an output for the shell derivation, so we 31 | // check it's dependencies instead. 32 | for (drv, outputs) in load_derive(&path)?.input_drvs { 33 | let parsed_drv = load_derive(&drv)?; 34 | for out_name in outputs { 35 | let name = &parsed_drv 36 | .outputs 37 | .iter() 38 | .find(|(name, _, _, _)| name == &out_name) 39 | .ok_or_else(|| { 40 | format!( 41 | "{}: output {:?} not found", 42 | drv.to_string_lossy(), 43 | out_name 44 | ) 45 | })? 46 | .1; 47 | if !Path::new(&name).exists() { 48 | return Err(format!("{}: not found", name.to_string_lossy())); 49 | } 50 | } 51 | } 52 | Ok(()) 53 | } 54 | 55 | fn load_derive>(path: P) -> Result { 56 | let data = read(path.as_ref()) 57 | .map_err(|e| format!("{}: !{}", path.as_ref().to_string_lossy(), e))?; 58 | parse_derive(&data).map(|a| a.1).map_err(|_| { 59 | format!("{}: failed to parse", path.as_ref().to_string_lossy()) 60 | }) 61 | } 62 | 63 | fn parse_derive(input: &[u8]) -> IResult<&[u8], Derivation> { 64 | let (input, values) = delimited( 65 | tag("Derive"), 66 | tuple(( 67 | preceded(char('('), parse_list(parse_output)), 68 | preceded(char(','), parse_list(parse_input_drv)), 69 | preceded(char(','), parse_list(parse_string)), 70 | preceded(char(','), parse_string), 71 | preceded(char(','), parse_string), 72 | preceded(char(','), parse_list(parse_string)), 73 | preceded(char(','), parse_list(parse_env)), 74 | )), 75 | preceded(char(')'), eof), 76 | )(input)?; 77 | 78 | let result = Derivation { 79 | outputs: values.0, 80 | input_drvs: values.1, 81 | input_srcs: values.2, 82 | platform: values.3, 83 | builder: values.4, 84 | args: values.5, 85 | env: values.6, 86 | }; 87 | Ok((input, result)) 88 | } 89 | 90 | fn parse_output( 91 | input: &[u8], 92 | ) -> IResult<&[u8], (OsString, OsString, OsString, OsString)> { 93 | tuple(( 94 | preceded(char('('), parse_string), 95 | preceded(char(','), parse_string), 96 | preceded(char(','), parse_string), 97 | delimited(char(','), parse_string, char(')')), 98 | ))(input) 99 | } 100 | 101 | fn parse_input_drv(input: &[u8]) -> IResult<&[u8], (OsString, Vec)> { 102 | tuple(( 103 | preceded(char('('), parse_string), 104 | delimited(char(','), parse_list(parse_string), char(')')), 105 | ))(input) 106 | } 107 | 108 | fn parse_env(input: &[u8]) -> IResult<&[u8], (OsString, OsString)> { 109 | tuple(( 110 | preceded(char('('), parse_string), 111 | delimited(char(','), parse_string, char(')')), 112 | ))(input) 113 | } 114 | 115 | fn parse_list(f: F) -> impl FnMut(I) -> IResult, E> 116 | where 117 | I: Clone + nom::InputLength + Slice> + InputIter, 118 | ::Item: AsChar, 119 | F: nom::Parser, 120 | E: nom::error::ParseError, 121 | { 122 | delimited(char('['), separated_list0(char(','), f), char(']')) 123 | } 124 | 125 | fn parse_string(input: &[u8]) -> IResult<&[u8], OsString> { 126 | let (input, parsed) = delimited( 127 | char('\"'), 128 | opt(escaped_transform( 129 | is_not("\\\""), 130 | '\\', 131 | alt(( 132 | value(b"\\" as &'static [u8], char('\\')), 133 | value(b"\"" as &'static [u8], char('"')), 134 | value(b"\n" as &'static [u8], char('n')), 135 | )), 136 | )), 137 | char('\"'), 138 | )(input)?; 139 | Ok((input, OsString::from_vec(parsed.unwrap_or_default()))) 140 | } 141 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use crate::args::Args; 2 | use crate::bash::is_literal_bash_string; 3 | use crate::path_clean::PathClean; 4 | use crate::trace::Trace; 5 | use itertools::{chain, Itertools}; 6 | use nix::unistd::{access, AccessFlags}; 7 | use once_cell::sync::Lazy; 8 | use std::collections::{BTreeMap, HashSet}; 9 | use std::env::current_dir; 10 | use std::ffi::{OsStr, OsString}; 11 | use std::fs::{read, File}; 12 | use std::io::{Read, Write}; 13 | use std::os::unix::ffi::OsStrExt; 14 | use std::os::unix::prelude::OsStringExt; 15 | use std::os::unix::process::CommandExt; 16 | use std::os::unix::process::ExitStatusExt; 17 | use std::path::{Path, PathBuf}; 18 | use std::process::{exit, Command, Stdio}; 19 | use std::time::Instant; 20 | use tempfile::NamedTempFile; 21 | use ufcs::Pipe; 22 | 23 | mod args; 24 | mod bash; 25 | mod drv; 26 | mod nix_path; 27 | mod path_clean; 28 | mod shebang; 29 | mod trace; 30 | 31 | type EnvMap = BTreeMap; 32 | 33 | struct EnvOptions { 34 | env: EnvMap, 35 | bashopts: OsString, 36 | shellopts: OsString, 37 | } 38 | 39 | static XDG_DIRS: Lazy = Lazy::new(|| { 40 | xdg::BaseDirectories::with_prefix("cached-nix-shell") 41 | .expect("Can't get find base cache directory") 42 | }); 43 | 44 | /// Serialize environment variables in the same way as `env -0` does. 45 | fn serialize_env(env: &EnvMap) -> Vec { 46 | let mut vec = Vec::new(); 47 | for (k, v) in env { 48 | vec.extend(k.as_bytes()); 49 | vec.push(b'='); 50 | vec.extend(v.as_bytes()); 51 | vec.push(0); 52 | } 53 | vec 54 | } 55 | 56 | /// Deserealize environment variables from `env -0` format. 57 | fn deserealize_env(vec: Vec) -> EnvMap { 58 | vec.split(|&b| b == 0) 59 | .filter(|&var| !var.is_empty()) // last entry has trailing NUL 60 | .map(|var| { 61 | let pos = var.iter().position(|&x| x == b'=').unwrap(); 62 | ( 63 | OsStr::from_bytes(&var[0..pos]).to_owned(), 64 | OsStr::from_bytes(&var[pos + 1..]).to_owned(), 65 | ) 66 | }) 67 | .collect::>() 68 | } 69 | 70 | fn serialize_args(args: &[OsString]) -> Vec { 71 | let mut vec = Vec::new(); 72 | for arg in args { 73 | vec.extend(arg.as_bytes()); 74 | vec.push(0); 75 | } 76 | vec 77 | } 78 | 79 | fn serialize_vecs(vecs: &[&[u8]]) -> Vec { 80 | let mut vec = Vec::new(); 81 | for v in vecs { 82 | vec.extend(format!("{}\0", v.len()).as_str().as_bytes()); 83 | vec.extend(v.iter()); 84 | } 85 | vec 86 | } 87 | 88 | fn unwrap_or_errx(x: Result) -> T { 89 | match x { 90 | Ok(x) => x, 91 | Err(x) => { 92 | eprintln!("cached-nix-shell: {x}"); 93 | exit(1) 94 | } 95 | } 96 | } 97 | 98 | struct NixShellInput { 99 | pwd: PathBuf, 100 | env: EnvMap, 101 | args: Vec, 102 | weak_args: Vec, 103 | } 104 | 105 | struct NixShellOutput { 106 | env: EnvMap, 107 | trace: trace::Trace, 108 | drv: String, 109 | } 110 | 111 | fn minimal_essential_path() -> OsString { 112 | let required_binaries = ["tar", "gzip", "git", "nix-shell", "rm"]; 113 | 114 | fn which_dir(binary: &&str) -> Option { 115 | std::env::var_os("PATH") 116 | .as_ref() 117 | .unwrap() 118 | .pipe(std::env::split_paths) 119 | .find(|dir| { 120 | if access(&dir.join(binary), AccessFlags::X_OK).is_err() { 121 | return false; 122 | } 123 | 124 | if binary == &"nix-shell" { 125 | // Ignore our fake nix-shell. 126 | return !dir 127 | .join(binary) 128 | .canonicalize() 129 | .ok() 130 | .and_then(|x| x.file_name().map(|x| x.to_os_string())) 131 | .map(|x| x == "cached-nix-shell") 132 | .unwrap_or(true); 133 | } 134 | 135 | true 136 | }) 137 | } 138 | 139 | let required_paths = required_binaries 140 | .iter() 141 | .filter_map(which_dir) 142 | .collect::>(); 143 | 144 | // We can't just join_paths(required_paths) -- we need to preserve order 145 | std::env::var_os("PATH") 146 | .as_ref() 147 | .unwrap() 148 | .pipe(std::env::split_paths) 149 | .filter(|path_item| required_paths.contains(path_item)) 150 | .unique() 151 | .pipe(std::env::join_paths) 152 | .unwrap() 153 | } 154 | 155 | fn absolute_dirname(script_fname: &OsStr) -> PathBuf { 156 | Path::new(&script_fname) 157 | .parent() 158 | .expect("Can't get script dirname") 159 | .pipe(absolute) 160 | } 161 | 162 | fn absolute(path: &Path) -> PathBuf { 163 | if path.is_absolute() { 164 | path.to_path_buf() 165 | } else { 166 | // We do not use PathBuf::canonicalize() here since we do not want 167 | // symlink resolving. 168 | current_dir().expect("Can't get PWD").join(path).clean() 169 | } 170 | } 171 | 172 | fn args_to_inp(pwd: PathBuf, x: &Args) -> NixShellInput { 173 | let mut args = Vec::new(); 174 | 175 | args.push(OsString::from("--pure")); 176 | 177 | let env = { 178 | let mut clean_env = BTreeMap::new(); 179 | // Env vars to pass to `nix-shell --pure`. Changes to these variables 180 | // would invalidate the cache. 181 | let whitelist = &[ 182 | "HOME", 183 | "NIX_PATH", 184 | // tmp dir 185 | "TMPDIR", 186 | "XDG_RUNTIME_DIR", 187 | // ssl-related 188 | "CURL_CA_BUNDLE", 189 | "GIT_SSL_CAINFO", 190 | "NIX_SSL_CERT_FILE", 191 | "SSL_CERT_FILE", 192 | // Necessary if nix build caches are accessed via a proxy 193 | "http_proxy", 194 | "https_proxy", 195 | "ftp_proxy", 196 | "all_proxy", 197 | "no_proxy", 198 | ]; 199 | for var in whitelist { 200 | if let Some(val) = std::env::var_os(var) { 201 | clean_env.insert(OsString::from(var), val); 202 | } 203 | } 204 | for var in x.keep.iter() { 205 | if let Some(val) = std::env::var_os(var) { 206 | clean_env.insert(var.clone(), val); 207 | args.push("--keep".into()); 208 | args.push(var.clone()); 209 | } 210 | } 211 | clean_env.insert(OsString::from("PATH"), minimal_essential_path()); 212 | clean_env 213 | }; 214 | 215 | args.extend(x.other_kw.clone()); 216 | args.push(OsString::from("--")); 217 | args.extend(x.rest.clone()); 218 | 219 | NixShellInput { 220 | pwd, 221 | env, 222 | args, 223 | weak_args: x.weak_kw.clone(), 224 | } 225 | } 226 | 227 | fn run_nix_shell(inp: &NixShellInput) -> NixShellOutput { 228 | let trace_file = NamedTempFile::new().expect("can't create temporary file"); 229 | 230 | let env_file = NamedTempFile::new().expect("can't create temporary file"); 231 | let env_cmd = [ 232 | b"{ printf \"BASHOPTS=%s\\0SHELLOPTS=%s\\0\" \"${BASHOPTS-}\" \"${SHELLOPTS-}\" ; env -0; } >", 233 | bash::quote(env_file.path().as_os_str().as_bytes()).as_slice(), 234 | ] 235 | .concat(); 236 | 237 | let env = { 238 | let status = Command::new(concat!(env!("CNS_NIX"), "nix-shell")) 239 | .arg("--run") 240 | .arg(OsStr::from_bytes(&env_cmd)) 241 | .args(&inp.weak_args) 242 | .args(&inp.args) 243 | .stderr(std::process::Stdio::inherit()) 244 | .current_dir(&inp.pwd) 245 | .env_clear() 246 | .envs(&inp.env) 247 | .env("LD_PRELOAD", env!("CNS_TRACE_NIX_SO")) 248 | .env("DYLD_INSERT_LIBRARIES", env!("CNS_TRACE_NIX_SO")) 249 | .env("TRACE_NIX", trace_file.path()) 250 | .stdin(Stdio::null()) 251 | .status() 252 | .expect("failed to execute nix-shell"); 253 | if !status.success() { 254 | eprintln!("cached-nix-shell: nix-shell: {status}"); 255 | let code = status 256 | .code() 257 | .or_else(|| status.signal().map(|x| x + 127)) 258 | .unwrap_or(255); 259 | exit(code); 260 | } 261 | let mut env = read(env_file.path()) 262 | .expect("can't read an environment file") 263 | .pipe(deserealize_env); 264 | // Drop session variables exported by bash 265 | env.remove(OsStr::new("OLDPWD")); 266 | env.remove(OsStr::new("PWD")); 267 | env.remove(OsStr::new("SHLVL")); 268 | env.remove(OsStr::new("_")); 269 | env 270 | }; 271 | 272 | let env_out = env 273 | .get(OsStr::new("out")) 274 | .expect("expected to have `out` environment variable"); 275 | 276 | let mut trace_file = 277 | trace_file.reopen().expect("can't reopen temporary file"); 278 | let mut trace_data = Vec::new(); 279 | trace_file 280 | .read_to_end(&mut trace_data) 281 | .expect("Can't read trace file"); 282 | let trace = Trace::load_raw(trace_data); 283 | if trace.check_for_changes() { 284 | eprintln!("cached-nix-shell: some files are already updated, cache won't be reused"); 285 | } 286 | std::mem::drop(trace_file); 287 | 288 | let drv: String = { 289 | // nix 2.3 290 | let mut exec = Command::new(concat!(env!("CNS_NIX"), "nix")) 291 | .arg("show-derivation") 292 | .arg(env_out) 293 | .output() 294 | .expect("failed to execute nix show-derivation"); 295 | let mut stderr = exec.stderr.clone(); 296 | if !exec.status.success() { 297 | // nix 2.4 298 | exec = Command::new(concat!(env!("CNS_NIX"), "nix")) 299 | .arg("show-derivation") 300 | .arg("--extra-experimental-features") 301 | .arg("nix-command") 302 | .arg(env_out) 303 | .output() 304 | .expect("failed to execute nix show-derivation"); 305 | stderr.extend(b"\n"); 306 | stderr.extend(exec.stderr); 307 | } 308 | if !exec.status.success() { 309 | eprintln!( 310 | "cached-nix-shell: failed to execute nix show-derivation" 311 | ); 312 | let _ = std::io::stderr().write_all(&stderr); 313 | exit(1); 314 | } 315 | 316 | // Path to .drv file is always in ASCII, so no information is lost. 317 | let output = String::from_utf8_lossy(&exec.stdout); 318 | let output: serde_json::Value = 319 | serde_json::from_str(&output).expect("failed to parse json"); 320 | // The first key of the toplevel object contains the path to .drv file. 321 | let (drv, _) = output.as_object().unwrap().into_iter().next().unwrap(); 322 | drv.clone() 323 | }; 324 | 325 | NixShellOutput { env, trace, drv } 326 | } 327 | 328 | fn run_script( 329 | fname: OsString, 330 | nix_shell_args: Vec, 331 | script_args: Vec, 332 | ) { 333 | let nix_shell_args = Args::parse(nix_shell_args, true).pipe(unwrap_or_errx); 334 | let inp = args_to_inp(absolute_dirname(&fname), &nix_shell_args); 335 | let env = cached_shell_env(nix_shell_args.pure, &inp); 336 | 337 | let exec = if is_literal_bash_string(nix_shell_args.interpreter.as_bytes()) 338 | { 339 | // eprintln!("Interpreter is a literal string, executing directly"); 340 | Command::new(nix_shell_args.interpreter) 341 | .arg(fname) 342 | .args(script_args) 343 | .env_clear() 344 | .envs(&env.env) 345 | .exec() 346 | } else { 347 | // eprintln!("Interpreter is bash command, executing 'bash -c'"); 348 | let mut exec_string = OsString::new(); 349 | exec_string.push("exec "); 350 | exec_string.push(nix_shell_args.interpreter); 351 | exec_string.push(r#" "$@""#); 352 | Command::new("bash") 353 | .arg("-c") 354 | .arg(exec_string) 355 | .arg("cached-nix-shell-bash") // corresponds to "$0" inside '-i' 356 | .arg(fname) 357 | .args(script_args) 358 | .env_clear() 359 | .envs(&env.env) 360 | .exec() 361 | }; 362 | 363 | eprintln!("cached-nix-shell: couldn't run: {exec:?}"); 364 | exit(1); 365 | } 366 | 367 | fn run_from_args(args: Vec) { 368 | let mut args = Args::parse(args, false).pipe(unwrap_or_errx); 369 | 370 | // Normalize PWD. 371 | // References: 372 | // https://github.com/NixOS/nix/blob/2.3.10/src/libexpr/common-eval-args.cc#L46-L57 373 | // https://github.com/NixOS/nix/blob/2.3.10/src/nix-build/nix-build.cc#L279-L291 374 | let nix_shell_pwd = if nix_path::contains_relative_paths(&args) { 375 | // in: nix-shell -I . "" 376 | // out: cd $PWD; nix-shell -I . "" 377 | current_dir().expect("Can't get PWD") 378 | } else if args.packages_or_expr { 379 | // in: nix-shel -p ... 380 | // out: cd /var/empty; nix-shell -p ... 381 | PathBuf::from(env!("CNS_VAR_EMPTY")) 382 | } else if let [arg] = &mut args.rest[..] { 383 | if arg == "" { 384 | // in: nix-shell "" 385 | // out: cd $PWD; nix-shell "" 386 | // nix-shell "" will use ./default.nix 387 | current_dir().expect("Can't get PWD") 388 | } else if arg.as_bytes().starts_with(b"<") 389 | && arg.as_bytes().ends_with(b">") 390 | || nix_path::is_pseudo_url(arg.as_bytes()) 391 | { 392 | // in: nix-shell '' 393 | // out: cd /var/empty; nix-shell '' 394 | // in: nix-shell http://... 395 | // out: cd /var/empty; nix-shell http://... 396 | PathBuf::from(env!("CNS_VAR_EMPTY")) 397 | } else if arg.as_bytes().ends_with(b"/") || Path::new(arg).is_dir() { 398 | // in: nix-shell /path/to/dir 399 | // out: cd /path/to/dir; nix-shell . 400 | let pwd = absolute(Path::new(arg)); 401 | *arg = OsString::from("."); 402 | pwd 403 | } else { 404 | // in: nix-shell /path/to/file 405 | // out: cd /path/to; nix-shell ./file 406 | let pwd = absolute_dirname(arg); 407 | *arg = PathBuf::from(&arg) 408 | .components() 409 | .next_back() 410 | .unwrap() 411 | .pipe(|x| PathBuf::from(".").join(x)) 412 | .into_os_string(); 413 | pwd 414 | } 415 | } else { 416 | // in: nix-shell 417 | // out: cd $PWD; nix-shell 418 | // nix-shell will use ./shell.nix or ./default.nix 419 | // in: nix-shell foo.nix bar.nix ... 420 | current_dir().expect("Can't get PWD") 421 | }; 422 | 423 | let inp = args_to_inp(nix_shell_pwd, &args); 424 | let env = cached_shell_env(args.pure, &inp); 425 | 426 | let (cmd, cmd_args) = match args.run { 427 | args::RunMode::InteractiveShell => { 428 | let mut args = vec!["--rcfile".into(), env!("CNS_RCFILE").into()]; 429 | args.append(build_bash_options(&env).as_mut()); 430 | ("bash".into(), args) 431 | } 432 | args::RunMode::Shell(cmd) => { 433 | let mut args = build_bash_options(&env); 434 | args.extend_from_slice(&["-c".into(), cmd]); 435 | ("bash".into(), args) 436 | } 437 | args::RunMode::Exec(cmd, cmd_args) => (cmd, cmd_args), 438 | }; 439 | 440 | let exec = Command::new(cmd) 441 | .args(cmd_args) 442 | .env_clear() 443 | .envs(&env.env) 444 | .exec(); 445 | eprintln!("cached-nix-shell: couldn't run: {exec:?}"); 446 | exit(1); 447 | } 448 | 449 | fn cached_shell_env(pure: bool, inp: &NixShellInput) -> EnvOptions { 450 | let inputs = serialize_vecs(&[ 451 | &serialize_env(&inp.env), 452 | &serialize_args(&inp.args), 453 | inp.pwd.as_os_str().as_bytes(), 454 | ]); 455 | 456 | let inputs_hash = blake3::hash(&inputs).to_hex().as_str().to_string(); 457 | 458 | let mut env = if let Some(env) = check_cache(&inputs_hash) { 459 | env 460 | } else { 461 | eprintln!("cached-nix-shell: updating cache"); 462 | let start = Instant::now(); 463 | let outp = run_nix_shell(inp); 464 | eprintln!("cached-nix-shell: done in {:?}", start.elapsed()); 465 | 466 | // TODO: use flock 467 | cache_write(&inputs_hash, "inputs", &inputs); 468 | cache_write(&inputs_hash, "env", &serialize_env(&outp.env)); 469 | cache_write(&inputs_hash, "trace", &outp.trace.serialize()); 470 | cache_symlink(&inputs_hash, "drv", &outp.drv); 471 | 472 | outp.env 473 | }; 474 | 475 | let shellopts = env.remove(OsStr::new("SHELLOPTS")).unwrap_or_default(); 476 | let bashopts = env.remove(OsStr::new("BASHOPTS")).unwrap_or_default(); 477 | env.insert(OsString::from("IN_CACHED_NIX_SHELL"), OsString::from("1")); 478 | 479 | EnvOptions { 480 | env: merge_env(if pure { env } else { merge_impure_env(env) }), 481 | shellopts, 482 | bashopts, 483 | } 484 | } 485 | 486 | // Merge ambient (impure) environment into cached env. 487 | fn merge_impure_env(mut env: EnvMap) -> EnvMap { 488 | let mut delim = EnvMap::new(); 489 | delim.insert(OsString::from("PATH"), OsString::from(":")); 490 | delim.insert(OsString::from("HOST_PATH"), OsString::from(":")); 491 | delim.insert(OsString::from("XDG_DATA_DIRS"), OsString::from(":")); 492 | 493 | // Set to "/no-cert-file.crt" by setup.sh for pure envs. 494 | env.remove(OsStr::new("SSL_CERT_FILE")); 495 | env.remove(OsStr::new("NIX_SSL_CERT_FILE")); 496 | 497 | env.insert(OsString::from("IN_NIX_SHELL"), OsString::from("impure")); 498 | 499 | for (var, val) in std::env::vars_os() { 500 | env.entry(var.clone()) 501 | .and_modify(|old_val| { 502 | if let Some(d) = delim.get(&var) { 503 | *old_val = OsString::from(OsStr::from_bytes( 504 | &[ 505 | old_val.as_os_str().as_bytes(), 506 | d.as_os_str().as_bytes(), 507 | val.as_os_str().as_bytes(), 508 | ] 509 | .concat(), 510 | )); 511 | } 512 | }) 513 | .or_insert_with(|| val); 514 | } 515 | 516 | env 517 | } 518 | 519 | fn merge_env(mut env: EnvMap) -> EnvMap { 520 | // These variables are always passed by the original nix-shell, regardless 521 | // of the --pure flag. 522 | let keep = &[ 523 | "USER", 524 | "LOGNAME", 525 | "DISPLAY", 526 | "WAYLAND_DISPLAY", 527 | "WAYLAND_SOCKET", 528 | "TERM", 529 | "NIX_SHELL_PRESERVE_PROMPT", 530 | "TZ", 531 | "PAGER", 532 | "SHLVL", 533 | ]; 534 | for var in keep { 535 | if let Some(vel) = std::env::var_os(var) { 536 | env.insert(OsString::from(var), vel); 537 | } 538 | } 539 | env 540 | } 541 | 542 | fn build_bash_options(env: &EnvOptions) -> Vec { 543 | // XXX: only check for options that are set by current stdenv and nix-shell. 544 | const BASH_OPTIONS: [&[u8]; 3] = 545 | [b"execfail", b"inherit_errexit", b"nullglob"]; 546 | const SHELL_OPTIONS: [&[u8]; 1] = [b"pipefail"]; 547 | chain!( 548 | env.bashopts 549 | .as_bytes() 550 | .split(|b| *b == b':') 551 | .filter(|opt| BASH_OPTIONS.contains(opt)) 552 | .map(|opt| vec!["-O".into(), OsString::from_vec(opt.to_vec())]), 553 | env.shellopts 554 | .as_bytes() 555 | .split(|b| *b == b':') 556 | .filter(|opt| SHELL_OPTIONS.contains(opt)) 557 | .map(|opt| vec!["-o".into(), OsString::from_vec(opt.to_vec())]), 558 | ) 559 | .flatten() 560 | .collect() 561 | } 562 | 563 | fn check_cache(hash: &str) -> Option> { 564 | let env_fname = XDG_DIRS.find_cache_file(format!("{hash}.env"))?; 565 | let drv_fname = XDG_DIRS.find_cache_file(format!("{hash}.drv"))?; 566 | let trace_fname = XDG_DIRS.find_cache_file(format!("{hash}.trace"))?; 567 | 568 | let env = read(env_fname).unwrap().pipe(deserealize_env); 569 | 570 | if let Err(e) = drv::derivation_is_ok(drv_fname) { 571 | eprintln!("cached-nix-shell: {}", e); 572 | return None; 573 | } 574 | 575 | let trace = read(trace_fname).unwrap().pipe(Trace::load_sorted); 576 | if trace.check_for_changes() { 577 | return None; 578 | } 579 | 580 | Some(env) 581 | } 582 | 583 | fn cache_write(hash: &str, ext: &str, text: &[u8]) { 584 | let f = || -> Result<(), std::io::Error> { 585 | let fname = XDG_DIRS.place_cache_file(format!("{hash}.{ext}"))?; 586 | let mut file = File::create(fname)?; 587 | file.write_all(text)?; 588 | Ok(()) 589 | }; 590 | match f() { 591 | Ok(_) => (), 592 | Err(e) => eprintln!("Warning: can't store cache: {e}"), 593 | } 594 | } 595 | 596 | fn cache_symlink(hash: &str, ext: &str, target: &str) { 597 | let f = || -> Result<(), std::io::Error> { 598 | let fname = XDG_DIRS.place_cache_file(format!("{hash}.{ext}"))?; 599 | let _ = std::fs::remove_file(&fname); 600 | std::os::unix::fs::symlink(target, &fname)?; 601 | Ok(()) 602 | }; 603 | match f() { 604 | Ok(_) => (), 605 | Err(e) => eprintln!("Warning: can't symlink to cache: {e}"), 606 | } 607 | } 608 | 609 | fn wrap(cmd: Vec) { 610 | if cmd.is_empty() { 611 | eprintln!("cached-nix-shell: command not specified"); 612 | eprintln!("usage: cached-nix-shell --wrap COMMAND ARGS..."); 613 | exit(1); 614 | } 615 | 616 | if access( 617 | Path::new(&format!("{}/nix-shell", env!("CNS_WRAP_PATH"))), 618 | AccessFlags::X_OK, 619 | ) 620 | .is_err() 621 | { 622 | eprintln!( 623 | "cached-nix-shell: couldn't wrap, {}/nix-shell is not executable", 624 | env!("CNS_WRAP_PATH") 625 | ); 626 | exit(1); 627 | } 628 | 629 | let new_path = [ 630 | env!("CNS_WRAP_PATH").as_bytes(), 631 | b":", 632 | std::env::var_os("PATH").unwrap().as_bytes(), 633 | ] 634 | .concat(); 635 | 636 | let exec = Command::new(&cmd[0]) 637 | .args(&cmd[1..]) 638 | .env("PATH", OsStr::from_bytes(&new_path)) 639 | .exec(); 640 | eprintln!("cached-nix-shell: couldn't run: {exec}"); 641 | exit(1); 642 | } 643 | 644 | fn main() { 645 | let argv: Vec = std::env::args_os().collect(); 646 | 647 | if argv.len() >= 2 && argv[1] == "--wrap" { 648 | wrap(std::env::args_os().skip(2).collect()); 649 | } 650 | 651 | if argv.len() >= 2 { 652 | let fname = &argv[1]; 653 | if let Some(nix_shell_args) = shebang::parse_script(fname) { 654 | run_script( 655 | fname.clone(), 656 | nix_shell_args, 657 | std::env::args_os().skip(2).collect(), 658 | ); 659 | } 660 | } 661 | run_from_args(std::env::args_os().skip(1).collect()); 662 | } 663 | -------------------------------------------------------------------------------- /src/nix_path.rs: -------------------------------------------------------------------------------- 1 | use crate::args::Args; 2 | use std::os::unix::ffi::OsStrExt; 3 | 4 | /// True if either $NIX_PATH or -I argument contain a relative path. 5 | pub fn contains_relative_paths(args: &Args) -> bool { 6 | let nix_path = std::env::var_os("NIX_PATH").unwrap_or("".into()); 7 | 8 | let include_nix_path = args.include_nix_path.iter().map(|x| x.as_bytes()); 9 | 10 | parse_nix_path(nix_path.as_bytes()) 11 | .into_iter() 12 | .chain(include_nix_path) 13 | .any(is_relative) 14 | } 15 | 16 | fn is_relative(b: &[u8]) -> bool { 17 | let pos = b 18 | .iter() 19 | .position(|&c| c == b'=') 20 | .map(|x| x + 1) 21 | .unwrap_or(0); 22 | b.get(pos) != Some(&b'/') && !is_pseudo_url(&b[pos..]) 23 | } 24 | 25 | /// Reference: https://github.com/NixOS/nix/blob/2.23.0/src/libexpr/eval-settings.cc#L9-L45 26 | fn parse_nix_path(s: &[u8]) -> Vec<&[u8]> { 27 | let mut res = Vec::new(); 28 | let mut p = 0; 29 | while p < s.len() { 30 | let start = p; 31 | let mut start2 = p; 32 | 33 | while p < s.len() && s[p] != b':' { 34 | if s[p] == b'=' { 35 | start2 = p + 1; 36 | } 37 | p += 1; 38 | } 39 | 40 | if p == s.len() { 41 | if p != start { 42 | res.push(&s[start..p]); 43 | } 44 | break; 45 | } 46 | 47 | if s[p] == b':' { 48 | if is_pseudo_url(&s[start2..]) { 49 | p += 1; 50 | while p < s.len() && s[p] != b':' { 51 | p += 1; 52 | } 53 | } 54 | res.push(&s[start..p]); 55 | if p == s.len() { 56 | break; 57 | } 58 | } 59 | 60 | p += 1; 61 | } 62 | 63 | res 64 | } 65 | 66 | /// Reference: https://github.com/NixOS/nix/blob/2.23.0/src/libexpr/eval-settings.cc#L79-L86 67 | pub fn is_pseudo_url(s: &[u8]) -> bool { 68 | let prefixes = &[ 69 | "channel:", 70 | "http://", 71 | "https://", 72 | "file://", 73 | "channel://", 74 | "git://", 75 | "s3://", 76 | "ssh://", 77 | "flake:", // Not in the original code 78 | ]; 79 | prefixes 80 | .iter() 81 | .any(|prefix| s.starts_with(prefix.as_bytes())) 82 | } 83 | 84 | #[cfg(test)] 85 | mod tests { 86 | use super::*; 87 | 88 | macro_rules! v { 89 | ( $($a:literal),* ) => {{ 90 | vec![ $( Vec::::from($a as &[_])),* ] 91 | }} 92 | } 93 | 94 | #[test] 95 | fn test_parse_nix_path() { 96 | assert_eq!(parse_nix_path(b"foo:bar:baz"), v![b"foo", b"bar", b"baz"]); 97 | assert_eq!( 98 | parse_nix_path(b"foo:bar=something:baz"), 99 | v![b"foo", b"bar=something", b"baz"] 100 | ); 101 | assert_eq!( 102 | parse_nix_path( 103 | b"foo:bar=https://something:baz=flake:something:qux" 104 | ), 105 | v![ 106 | b"foo", 107 | b"bar=https://something", 108 | b"baz=flake:something", 109 | b"qux" 110 | ] 111 | ); 112 | } 113 | 114 | #[test] 115 | fn test_is_relative() { 116 | assert!(is_relative(b"foo")); 117 | assert!(is_relative(b"foo=bar")); 118 | assert!(!is_relative(b"http://foo")); 119 | assert!(!is_relative(b"foo=/bar")); 120 | assert!(!is_relative(b"foo=http://bar")); 121 | assert!(!is_relative(b"foo=flake:bar")); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/path_clean.rs: -------------------------------------------------------------------------------- 1 | // Why I find [path_clean] crate unsuitable: since it operates on str/String, it 2 | // assumes that paths are valid utf8 strings. My implementation operates on 3 | // Path/PathBuf/OsStr thus it doesn't make any assumptions. 4 | // [path_clean]: https://docs.rs/path-clean/0.1.0/path_clean/ 5 | 6 | use std::ffi::OsStr; 7 | use std::path::{Path, PathBuf}; 8 | 9 | pub trait PathClean { 10 | fn clean(&self) -> PathBuf; 11 | } 12 | 13 | impl PathClean for Path { 14 | fn clean(&self) -> PathBuf { 15 | let mut res = Vec::new(); 16 | for elem in self { 17 | if elem == "/" { 18 | res.push(elem); 19 | } else if elem == "." { 20 | // do nothing 21 | } else if elem == ".." { 22 | if res.last().is_none() || res.last() == Some(&OsStr::new("..")) 23 | { 24 | res.push(elem); 25 | } else if res.last() == Some(&OsStr::new("/")) { 26 | // do nothing 27 | } else { 28 | res.pop(); 29 | } 30 | } else { 31 | res.push(elem); 32 | } 33 | } 34 | if res.is_empty() { 35 | res.push(OsStr::new(".")); 36 | } 37 | res.into_iter().collect() 38 | } 39 | } 40 | 41 | #[test] 42 | fn path_clean_test() { 43 | // Taken from https://golang.org/src/path/path_test.go 44 | let cases = [ 45 | // Already clean 46 | ("", "."), 47 | ("abc", "abc"), 48 | ("abc/def", "abc/def"), 49 | ("a/b/c", "a/b/c"), 50 | (".", "."), 51 | ("..", ".."), 52 | ("../..", "../.."), 53 | ("../../abc", "../../abc"), 54 | ("/abc", "/abc"), 55 | ("/", "/"), 56 | // Remove trailing slash 57 | ("abc/", "abc"), 58 | ("abc/def/", "abc/def"), 59 | ("a/b/c/", "a/b/c"), 60 | ("./", "."), 61 | ("../", ".."), 62 | ("../../", "../.."), 63 | ("/abc/", "/abc"), 64 | // Remove doubled slash 65 | ("abc//def//ghi", "abc/def/ghi"), 66 | ("//abc", "/abc"), 67 | ("///abc", "/abc"), 68 | ("//abc//", "/abc"), 69 | ("abc//", "abc"), 70 | // Remove . elements 71 | ("abc/./def", "abc/def"), 72 | ("/./abc/def", "/abc/def"), 73 | ("abc/.", "abc"), 74 | // Remove .. elements 75 | ("abc/def/ghi/../jkl", "abc/def/jkl"), 76 | ("abc/def/../ghi/../jkl", "abc/jkl"), 77 | ("abc/def/..", "abc"), 78 | ("abc/def/../..", "."), 79 | ("/abc/def/../..", "/"), 80 | ("abc/def/../../..", ".."), 81 | ("/abc/def/../../..", "/"), 82 | ("abc/def/../../../ghi/jkl/../../../mno", "../../mno"), 83 | // Combinations 84 | ("abc/./../def", "def"), 85 | ("abc//./../def", "def"), 86 | ("abc/../../././../def", "../../def"), 87 | ]; 88 | 89 | for (path, result) in cases.iter() { 90 | assert_eq!(Path::new(path).clean(), Path::new(result)); 91 | assert_eq!(Path::new(result).clean(), Path::new(result)); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/shebang.rs: -------------------------------------------------------------------------------- 1 | use bytelines::ByteLinesReader; 2 | use std::ffi::{OsStr, OsString}; 3 | use std::fs::File; 4 | use std::os::unix::ffi::OsStrExt; 5 | 6 | /// Parse script in the same way as nix-shell does. 7 | /// Reference: https://github.com/NixOS/nix/blob/2.3.1/src/nix-build/nix-build.cc#L113-L126 8 | pub fn parse_script(fname: &OsStr) -> Option> { 9 | let f = File::open(fname).ok()?; 10 | let reader = std::io::BufReader::new(&f); 11 | let mut lines = reader.byte_lines(); 12 | 13 | if !lines.next()?.ok()?.starts_with(b"#!") { 14 | return None; // First line isn't shebang 15 | } 16 | 17 | let mut result = Vec::new(); 18 | 19 | while let Some(line) = lines.next() { 20 | let line = line.unwrap(); 21 | if let Some(m) = re_nix_shell(line) { 22 | let mut items = shellwords(m) 23 | .into_iter() 24 | .map(|x| OsStr::from_bytes(&x).to_os_string()) 25 | .collect::>(); 26 | result.append(&mut items); 27 | } 28 | } 29 | 30 | Some(result) 31 | } 32 | 33 | /// Reference: https://github.com/NixOS/nix/blob/2.3.1/src/nix-build/nix-build.cc#L26-L68 34 | fn shellwords(s: &[u8]) -> Vec> { 35 | let mut res = Vec::new(); 36 | let mut it = 0; 37 | let mut begin = 0; 38 | let mut cur = Vec::new(); 39 | let mut state = true; 40 | loop { 41 | if state { 42 | if let Some(match_len) = re_whitespaces_len(&s[it..]) { 43 | cur.extend_from_slice(&s[begin..it]); 44 | res.push(cur); 45 | cur = Vec::new(); 46 | it += match_len; 47 | begin = it; 48 | } 49 | } 50 | match s.get(it) { 51 | Some(b'"') => { 52 | cur.extend_from_slice(&s[begin..it]); 53 | begin = it + 1; 54 | state = !state; 55 | } 56 | Some(b'\\') => { 57 | cur.extend_from_slice(&s[begin..it]); 58 | begin = it + 1; 59 | it += 1; 60 | } 61 | Some(_) => {} 62 | None => break, 63 | } 64 | it += 1; 65 | } 66 | cur.extend_from_slice(&s[begin..it]); 67 | if !cur.is_empty() { 68 | res.push(cur); 69 | } 70 | res 71 | } 72 | 73 | /// Characters that are matched by `\s` or isspace(3). 74 | const SPACES: &[u8; 6] = &[9, 10, 11, 12, 13, 32]; 75 | 76 | /// Match C++'s `std::regex("^#!\\s*nix-shell (.*)$")` and return `\1` 77 | fn re_nix_shell(mut line: &[u8]) -> Option<&[u8]> { 78 | if !line.starts_with(b"#!") { 79 | return None; 80 | } 81 | line = &line[b"#!".len()..]; 82 | 83 | while line.first().map(|c| SPACES.contains(c)) == Some(true) { 84 | line = &line[1..]; 85 | } 86 | 87 | if !line.starts_with(b"nix-shell ") { 88 | return None; 89 | } 90 | line = &line[b"nix-shell ".len()..]; 91 | 92 | Some(line) 93 | } 94 | 95 | /// Match C++'s `std::regex("^(\\s+).*")` and return length of `\1` 96 | fn re_whitespaces_len(line: &[u8]) -> Option { 97 | let mut result = 0; 98 | while line.get(result).map(|c| SPACES.contains(c)) == Some(true) { 99 | result += 1 100 | } 101 | Some(result).filter(|&x| x != 0) 102 | } 103 | 104 | #[cfg(test)] 105 | mod tests { 106 | use super::shellwords; 107 | macro_rules! v { 108 | ( $($a:literal),* ) => {{ 109 | vec![ $( Vec::::from($a as &[_])),* ] 110 | }} 111 | } 112 | #[test] 113 | fn it_works() { 114 | assert_eq!(shellwords(b"foo bar baz"), v![b"foo", b"bar", b"baz"],); 115 | 116 | assert_eq!( 117 | shellwords(br#"foo "bar baz" qoox"#), 118 | v![b"foo", b"bar baz", b"qoox"], 119 | ); 120 | 121 | assert_eq!(shellwords(br#"foo "bar baz "#), v![b"foo", b"bar baz "],); 122 | 123 | assert_eq!( 124 | shellwords(br#"foo \"bar baz"#), 125 | v![b"foo", br#""bar"#, b"baz"], 126 | ); 127 | 128 | assert_eq!( 129 | shellwords(br#"foo "bar\"baz" qoox"#), 130 | v![b"foo", br#"bar"baz"#, b"qoox"], 131 | ); 132 | 133 | assert_eq!( 134 | shellwords(br#"foo bar" "baz qoox"#), 135 | v![b"foo", b"bar baz", b"qoox"], 136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/trace.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | use std::collections::btree_map::Entry; 3 | use std::collections::BTreeMap; 4 | use std::ffi::{OsStr, OsString}; 5 | use std::fs::{read, read_dir, read_link, symlink_metadata}; 6 | use std::io::ErrorKind; 7 | use std::os::unix::ffi::OsStrExt; 8 | use ufcs::Pipe; 9 | 10 | pub struct Trace(Vec<(Vec, Vec)>); 11 | 12 | impl Trace { 13 | /// Load trace from a vector of bytes, as returned by `trace-nix.so`. 14 | /// Sort and deduplicate entries. Remove entries inside temporary 15 | /// directories created by fetchTarball. 16 | pub fn load_raw(vec: Vec) -> Trace { 17 | let mut result = BTreeMap::, Vec>::new(); 18 | let mut inbetween = BTreeMap::, Vec<(Vec, Vec)>>::new(); 19 | 20 | // In between `tTMPDIR\0+\0` and `tTMPDIR\0-\0`, ignore all entries 21 | // starting with `TMPDIR/`, including `TMPDIR` itself, if `TMPDIR` is 22 | // no longer exists. This corresponds to the temporary directory created 23 | // by fetchTarball. 24 | 25 | 'outer: for (key, value) in vec 26 | .split(|&b| b == 0) 27 | .filter(|&fname| !fname.is_empty()) // last entry has trailing NUL 28 | .map(Vec::from) 29 | .tuples::<(_, _)>() 30 | { 31 | let fname = OsStr::from_bytes(&key[1..]); 32 | 33 | if key.first() == Some(&b't') { 34 | match value.as_slice() { 35 | b"+" => { 36 | // Handle mkdir 37 | if symlink_metadata(fname).is_ok() { 38 | // Unlikely: temporary directory still exists 39 | continue; 40 | } 41 | match inbetween.entry(key[1..].to_vec()) { 42 | Entry::Vacant(entry) => { 43 | // Likely. 44 | entry.insert(Vec::new()); 45 | } 46 | Entry::Occupied(mut entry) => { 47 | // Unlikely: the directory with the same 48 | // name was created twice. 49 | for (k, v) in entry.get_mut().drain(..) { 50 | result.insert(k, v); 51 | } 52 | *entry.get_mut() = Vec::new(); 53 | } 54 | } 55 | } 56 | b"-" => { 57 | // Handle unlinkat 58 | inbetween.remove(&key[1..]); 59 | } 60 | _ => panic!("Unexpected"), 61 | } 62 | } else { 63 | for (k, v) in inbetween.iter_mut() { 64 | if k == &key[1..] 65 | || key[1..].starts_with(k) 66 | && key.get(k.len() + 1) == Some(&b'/') 67 | { 68 | v.push((key.clone(), value.clone())); 69 | continue 'outer; 70 | } 71 | } 72 | result.insert(key, value); 73 | } 74 | } 75 | 76 | for (_, v) in inbetween { 77 | for (k, v) in v { 78 | result.insert(k, v); 79 | } 80 | } 81 | 82 | Trace(result.into_iter().collect()) 83 | } 84 | 85 | /// Load trace from a vector of bytes, as stored in the cache. 86 | pub fn load_sorted(vec: Vec) -> Trace { 87 | vec.split(|&b| b == 0) 88 | .filter(|&fname| !fname.is_empty()) // last entry has trailing NUL 89 | .map(Vec::from) 90 | .tuples::<(_, _)>() 91 | .collect::>() 92 | .pipe(Trace) 93 | } 94 | 95 | pub fn serialize(&self) -> Vec { 96 | let mut result = Vec::::new(); 97 | for (a, b) in &self.0 { 98 | result.push(0); 99 | result.extend(a); 100 | result.push(0); 101 | result.extend(b); 102 | } 103 | result 104 | } 105 | 106 | /// Return true if trace doesn't match (i.e. some file is changed) 107 | pub fn check_for_changes(&self) -> bool { 108 | for (k, v) in &self.0 { 109 | if check_item_updated(k, v) { 110 | return true; 111 | } 112 | } 113 | false 114 | } 115 | } 116 | 117 | fn check_item_updated(k: &[u8], v: &[u8]) -> bool { 118 | let tmp: OsString; 119 | let fname = OsStr::from_bytes(&k[1..]); 120 | let res = match k.iter().next() { 121 | Some(b's') => match symlink_metadata(fname) { 122 | Err(_) => OsStr::new("-"), 123 | Ok(md) => { 124 | if md.file_type().is_symlink() { 125 | let mut l = OsString::from("l"); 126 | l.push(read_link(fname).expect("Can't read link")); 127 | tmp = l; 128 | tmp.as_os_str() 129 | } else if md.file_type().is_dir() { 130 | OsStr::new("d") 131 | } else { 132 | OsStr::new("+") 133 | } 134 | } 135 | }, 136 | Some(b'f') => match read(fname) { 137 | Ok(data) => { 138 | tmp = OsString::from( 139 | &blake3::hash(&data).to_hex().as_str()[..32], 140 | ); 141 | tmp.as_os_str() 142 | } 143 | Err(ref e) if e.kind() == ErrorKind::NotFound => OsStr::new("-"), 144 | Err(_) => OsStr::new("e"), 145 | }, 146 | Some(b'd') => { 147 | tmp = hash_dir(fname); 148 | tmp.as_os_str() 149 | } 150 | _ => panic!("Unexpected"), 151 | }; 152 | 153 | if res.as_bytes() != v { 154 | eprintln!( 155 | "cached-nix-shell: {:?}: expected {:?}, got {:?}", 156 | fname, 157 | OsStr::from_bytes(v), 158 | res 159 | ); 160 | return true; 161 | } 162 | false 163 | } 164 | 165 | fn hash_dir(fname: &OsStr) -> OsString { 166 | let entries = match read_dir(fname) { 167 | Ok(x) => x, 168 | Err(_) => return OsString::from("-"), 169 | }; 170 | 171 | let mut hasher = blake3::Hasher::new(); 172 | entries 173 | .filter_map(|entry| { 174 | let entry = entry.ok()?; 175 | let typ = match entry.file_type() { 176 | Ok(typ) => { 177 | if typ.is_symlink() { 178 | b'l' 179 | } else if typ.is_file() { 180 | b'f' 181 | } else if typ.is_dir() { 182 | b'd' 183 | } else { 184 | b'u' 185 | } 186 | } 187 | Err(_) => return None, 188 | }; 189 | Some([entry.file_name().as_bytes(), &[b'=', typ, 0]].concat()) 190 | }) 191 | .sorted() 192 | .for_each(|entry| { 193 | hasher.update(&entry); 194 | }); 195 | OsString::from(&hasher.finalize().to_hex().as_str()[..32]) 196 | } 197 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | /tmp 2 | -------------------------------------------------------------------------------- /tests/.shellcheckrc: -------------------------------------------------------------------------------- 1 | shell=dash 2 | 3 | # SC2016: Expressions don't expand in single quotes, use double quotes for that. 4 | # cached-nix-shell -p --run 'echo $x' 5 | # ^-------^ 6 | disable=SC2016 7 | 8 | # SC2119: Use run_inline "$@" if function's $1 should mean script's $1. 9 | # run_inline << 'EOF' 10 | # ^-----------------^ 11 | disable=SC2119 12 | -------------------------------------------------------------------------------- /tests/lib.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | export XDG_CACHE_HOME=$PWD/tmp/cache 4 | rm -rf ./tmp 5 | mkdir -p ./tmp 6 | 7 | printf "\n\33[1m* Test file %s\33[m\n" "$0" 8 | 9 | trap 'at_exit $?' EXIT 10 | begin_t=$(date +%s) 11 | 12 | at_exit() { 13 | local rc=$(($? || result)) 14 | local end_t 15 | end_t=$(date +%s) 16 | set -- tmp/cache/cached-nix-shell/*.env 17 | [ -f "$1" ] || shift 18 | printf "\33[1m* rc:%s seconds:%s entries:%s\33[m\n" \ 19 | "$rc" "$((end_t - begin_t))" "$#" 20 | exit "$rc" 21 | } 22 | 23 | result=0 24 | 25 | put() { 26 | if [ "$#" = 1 ]; then 27 | cat > "$1" 28 | elif [ "$#" = 2 ] && [ "$1" = "+x" ]; then 29 | cat > "$2" 30 | chmod +x "$2" 31 | fi 32 | } 33 | 34 | run() { 35 | rm -f tmp/time tmp/out tmp/err 36 | local testtmp=$PWD/tmp 37 | printf "\33[33m * Running %s\33[m\n" "$*" 38 | ( 39 | if [ "$1" = "--chdir" ]; then 40 | cd "$2" 41 | shift 2 42 | fi 43 | command time -p -o $testtmp/time -- "$@" 44 | ) 2>&1 > tmp/out | tee tmp/err 45 | } 46 | 47 | inline=0 48 | run_inline() { 49 | put +x ./tmp/inline$inline 50 | run ./tmp/inline$inline "$@" 51 | inline=$((inline+1)) 52 | } 53 | 54 | not() { 55 | ! "$@" 56 | } 57 | 58 | skip= 59 | 60 | check() { 61 | local text 62 | text=$1 63 | shift 64 | if "$@"; then 65 | printf "\33[32m + %s\33[m\n" "$text" 66 | elif [ "$skip" ]; then 67 | printf "\33[31;2m - (ignore) %s\33[m\n" "$text" 68 | else 69 | printf "\33[31m - %s\33[m\n" "$text" 70 | result=1 71 | fi 72 | } 73 | 74 | check_contains() { check "contains $1" grep -q "$1" tmp/out; } 75 | check_stderr_contains() { check "contains $1" grep -q "$1" tmp/err; } 76 | check_slow() { 77 | check "slow ($(sed 's/.* //;q' tmp/time))" \ 78 | grep -q "^cached-nix-shell: updating cache$" tmp/err 79 | } 80 | check_fast() { 81 | check "fast ($(sed 's/.* //;q' tmp/time))" \ 82 | not grep -q "^cached-nix-shell: updating cache$" tmp/err 83 | } 84 | 85 | skip() { 86 | local skip= 87 | ! eval "$1" || skip=1 88 | shift 89 | "$@" 90 | } 91 | -------------------------------------------------------------------------------- /tests/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # Check prerequisites 6 | hash cached-nix-shell cat chmod cmp cp \ 7 | date diff env grep ln mkdir rev rm tail tee time touch 8 | 9 | trap 'exit 130' INT 10 | 11 | case "$0" in 12 | */*) cd -- "${0%/*}";; 13 | esac 14 | 15 | echo "Testing $(command -v cached-nix-shell)" 16 | 17 | result=0 18 | 19 | if [ $# = 0 ]; then 20 | set -- ./t[0-9]*.sh 21 | fi 22 | 23 | for t in "$@"; do 24 | sh -- "$t" || result=1 25 | done 26 | 27 | if [ "$result" = 0 ]; then 28 | printf "\n\33[32mAll tests passed\33[m\n" 29 | else 30 | printf "\n\33[31mSome tests failed\33[m\n" 31 | fi 32 | rm -rf tmp 33 | exit $result 34 | -------------------------------------------------------------------------------- /tests/t01-lua.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . ./lib.sh 3 | 4 | put +x ./tmp/lua.sh << 'EOF' 5 | #!/usr/bin/env cached-nix-shell 6 | #! nix-shell -i sh -p lua 7 | 8 | lua -v 9 | EOF 10 | 11 | put +x ./tmp/luajit.sh << 'EOF' 12 | #!/usr/bin/env cached-nix-shell 13 | #! nix-shell -i sh -p luajit 14 | 15 | lua -v 16 | EOF 17 | 18 | put +x ./tmp/lua.lua << 'EOF' 19 | #! /usr/bin/env cached-nix-shell 20 | --[[ 21 | #! nix-shell -i lua -p lua 22 | ]] 23 | 24 | print(6 * 7) 25 | EOF 26 | 27 | 28 | run ./tmp/lua.sh 29 | check_contains "Lua.org" 30 | check_slow 31 | 32 | run ./tmp/lua.sh 33 | check_contains "Lua.org" 34 | check_fast 35 | 36 | run ./tmp/luajit.sh 37 | check_contains "https\?://luajit.org/" 38 | check_slow 39 | 40 | run ./tmp/luajit.sh 41 | check_contains "https\?://luajit.org/" 42 | check_fast 43 | 44 | run ./tmp/lua.lua 45 | check_contains "42" 46 | check_fast 47 | 48 | 49 | 50 | run cached-nix-shell -E 'with import { }; mkShell { buildInputs = [ lua ]; }' --run 'lua -v' 51 | check_contains "Lua.org" 52 | check_slow 53 | 54 | run cached-nix-shell -E 'with import { }; mkShell { buildInputs = [ lua ]; }' --run 'lua -v' 55 | check_contains "Lua.org" 56 | check_fast 57 | 58 | run cached-nix-shell -E 'with import { }; mkShell { buildInputs = [ luajit ]; }' --run 'lua -v' 59 | check_contains "https\?://luajit.org/" 60 | check_slow 61 | 62 | run cached-nix-shell -E 'with import { }; mkShell { buildInputs = [ luajit ]; }' --run 'lua -v' 63 | check_contains "https\?://luajit.org/" 64 | check_fast 65 | 66 | 67 | 68 | # Check various paths to use the same .nix file. 69 | put +x ./tmp/lua.nix << 'EOF' 70 | with import { }; mkShell { buildInputs = [ lua ]; } 71 | EOF 72 | 73 | run_inline << 'EOF' 74 | #! /usr/bin/env cached-nix-shell 75 | #! nix-shell -i sh ./lua.nix 76 | lua -v 77 | EOF 78 | check_contains "Lua.org" 79 | 80 | run cached-nix-shell ./tmp/lua.nix --run 'lua -v' 81 | check_contains "Lua.org" 82 | check_fast 83 | 84 | run --chdir tmp cached-nix-shell ./lua.nix --run 'lua -v' 85 | check_contains "Lua.org" 86 | check_fast 87 | 88 | run --chdir tmp cached-nix-shell lua.nix --run 'lua -v' 89 | check_contains "Lua.org" 90 | check_fast 91 | 92 | run --chdir / cached-nix-shell "$PWD/tmp/lua.nix" --run 'lua -v' 93 | check_contains "Lua.org" 94 | check_fast 95 | 96 | 97 | 98 | # Script with a trailing space in the nix-shell options 99 | run_inline << 'EOF' 100 | #!/usr/bin/env cached-nix-shell 101 | #! nix-shell -i sh -p lua 102 | lua -v 103 | EOF 104 | check_contains "Lua.org" 105 | -------------------------------------------------------------------------------- /tests/t02-file-dep.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . ./lib.sh 3 | # Check that cache in invalidated when indirecly used .nix file is updated. 4 | 5 | put ./tmp/small.nix << 'EOF' 6 | with import { }; 7 | mkShell { VAR = import ./foo.nix; } 8 | EOF 9 | 10 | 11 | echo '"val1"' > ./tmp/foo.nix 12 | run cached-nix-shell ./tmp/small.nix --run 'echo $VAR' 13 | check_contains '^val1$' 14 | check_slow 15 | 16 | run cached-nix-shell ./tmp/small.nix --run 'echo $VAR' 17 | check_contains '^val1$' 18 | check_fast 19 | 20 | echo '"val2"' > ./tmp/foo.nix 21 | run cached-nix-shell ./tmp/small.nix --run 'echo $VAR' 22 | check_contains '^val2$' 23 | check_slow 24 | -------------------------------------------------------------------------------- /tests/t03-path-pure-impure.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . ./lib.sh 3 | 4 | mkdir -p tmp/bin 5 | 6 | put +x tmp/bin/some_bin << 'EOF' 7 | #!/bin/sh 8 | echo running-some-bin 9 | EOF 10 | 11 | export PATH=$PWD/tmp/bin:$PATH 12 | 13 | 14 | run_inline << 'EOF' 15 | #!/usr/bin/env cached-nix-shell 16 | #! nix-shell -i sh -p hello 17 | 18 | if command -v some_bin >/dev/null 2>&1; then 19 | some_bin 20 | else 21 | echo cant-find-some-bin 22 | fi 23 | hello 24 | EOF 25 | check_contains "running-some-bin" 26 | check_contains "Hello, world!" 27 | 28 | 29 | run_inline << 'EOF' 30 | #!/usr/bin/env cached-nix-shell 31 | #! nix-shell -i sh -p hello --pure 32 | 33 | if command -v some_bin >/dev/null 2>&1; then 34 | some_bin 35 | else 36 | echo cant-find-some-bin 37 | fi 38 | hello 39 | EOF 40 | check_contains "cant-find-some-bin" 41 | check_contains "Hello, world!" 42 | -------------------------------------------------------------------------------- /tests/t04-env-pure-impure.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . ./lib.sh 3 | 4 | put tmp/prefix.nix << 'EOF' 5 | with import { }; 6 | let 7 | add-prefix = stdenv.mkDerivation { 8 | name = "cached-nix-shell-test-add-prefix"; 9 | unpackPhase = ":"; 10 | installPhase = ":"; 11 | setupHook = writeText "setup-hook" '' 12 | export FOO=prefix:$FOO 13 | ''; 14 | }; 15 | in mkShell { buildInputs = [ add-prefix ]; } 16 | EOF 17 | 18 | export FOO=foo-value 19 | unset BAR 20 | unset TERM 21 | 22 | 23 | run cached-nix-shell -p --run 'echo ${FOO-doesnt-have-foo}' 24 | check_contains "^foo-value$" 25 | 26 | run cached-nix-shell -p --pure --run 'echo ${FOO-doesnt-have-foo}' 27 | check_contains "^doesnt-have-foo$" 28 | check_fast 29 | 30 | run cached-nix-shell -p --pure --keep FOO --run 'echo ${FOO-doesnt-have-foo}' 31 | check_contains "^foo-value$" 32 | # TODO: this should not invalidate the cache 33 | skip true check_fast 34 | 35 | 36 | run cached-nix-shell ./tmp/prefix.nix --keep FOO \ 37 | --run 'echo ${FOO-doesnt-have-foo}' 38 | check_contains "^prefix:foo-value$" 39 | 40 | run cached-nix-shell ./tmp/prefix.nix --pure --keep FOO \ 41 | --run 'echo ${FOO-doesnt-have-foo}' 42 | check_contains "^prefix:foo-value$" 43 | check_fast 44 | 45 | export BAR=bar-value 46 | run cached-nix-shell ./tmp/prefix.nix --pure --keep FOO --keep BAR \ 47 | --run 'echo ${FOO-doesnt-have-foo}; echo ${BAR-doesnt-have-bar}' 48 | check_contains "^prefix:foo-value$" 49 | check_contains "^bar-value$" 50 | # TODO: this should not invalidate the cache 51 | skip true check_fast 52 | 53 | 54 | run env TERM=term-value cached-nix-shell -p --pure \ 55 | --run 'echo ${TERM-doesnt-have-term}' 56 | check_contains "^term-value$" 57 | check_fast 58 | -------------------------------------------------------------------------------- /tests/t05-attr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . ./lib.sh 3 | # Test -A / --attr handling 4 | 5 | put ./tmp/foobar.nix << 'EOF' 6 | with import { }; { 7 | foo = mkShell { 8 | buildInputs = [ 9 | (stdenv.mkDerivation { 10 | name = "cached-nix-shell-test-foo"; 11 | unpackPhase = ":"; 12 | installPhase = '' 13 | mkdir -p $out/bin 14 | echo 'echo i-am-foo' > $out/bin/foo 15 | chmod +x $out/bin/foo 16 | ''; 17 | }) 18 | ]; 19 | }; 20 | bar = mkShell { 21 | buildInputs = [ 22 | (stdenv.mkDerivation { 23 | name = "cached-nix-shell-test-bar"; 24 | unpackPhase = ":"; 25 | installPhase = '' 26 | mkdir -p $out/bin 27 | echo 'echo i-am-bar' > $out/bin/bar 28 | chmod +x $out/bin/bar 29 | ''; 30 | }) 31 | ]; 32 | }; 33 | } 34 | EOF 35 | 36 | 37 | run_inline << 'EOF' 38 | #!/usr/bin/env cached-nix-shell 39 | #! nix-shell -i sh -A foo ./foobar.nix 40 | 41 | if command -v foo >/dev/null 2>&1; then 42 | foo 43 | else 44 | echo cant-find-foo 45 | fi 46 | 47 | if command -v bar >/dev/null 2>&1; then 48 | bar 49 | else 50 | echo cant-find-bar 51 | fi 52 | EOF 53 | check_contains "i-am-foo" 54 | check_contains "cant-find-bar" 55 | -------------------------------------------------------------------------------- /tests/t06-readdir.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . ./lib.sh 3 | 4 | put ./tmp/readdir.nix << 'EOF' 5 | with import { }; 6 | mkShell { x = builtins.toJSON (builtins.readDir ./dir); } 7 | EOF 8 | 9 | 10 | mkdir -p tmp/dir/a 11 | ln -s c tmp/dir/b 12 | touch tmp/dir/c 13 | run cached-nix-shell ./tmp/readdir.nix --run 'echo $x' 14 | check_contains '^{"a":"directory","b":"symlink","c":"regular"}$' 15 | check_slow 16 | 17 | run cached-nix-shell ./tmp/readdir.nix --run 'echo $x' 18 | check_contains '^{"a":"directory","b":"symlink","c":"regular"}$' 19 | check_fast 20 | 21 | rm tmp/dir/b 22 | touch tmp/dir/b 23 | run cached-nix-shell ./tmp/readdir.nix --run 'echo $x' 24 | check_contains '^{"a":"directory","b":"regular","c":"regular"}$' 25 | check_slow 26 | -------------------------------------------------------------------------------- /tests/t07-implicit-default-nix.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . ./lib.sh 3 | 4 | put +x ./tmp/lua.nix << 'EOF' 5 | with import { }; mkShell { buildInputs = [ lua ]; } 6 | EOF 7 | 8 | put +x ./tmp/luajit.nix << 'EOF' 9 | with import { }; mkShell { buildInputs = [ luajit ]; } 10 | EOF 11 | 12 | mkdir -p ./tmp/foo 13 | cp ./tmp/lua.nix ./tmp/foo/default.nix 14 | # ./tmp/foo is a directory containing ./tmp/foo/default.nix 15 | 16 | run cached-nix-shell ./tmp/foo --run 'lua -v' 17 | check_contains "Lua.org" 18 | check_slow 19 | 20 | 21 | rm -rf ./tmp/foo 22 | cp ./tmp/luajit.nix ./tmp/foo 23 | # now ./tmp/foo is a plain .nix file 24 | 25 | run cached-nix-shell ./tmp/foo --run 'lua -v' 26 | check_contains "https\?://luajit.org/" 27 | check_slow 28 | 29 | 30 | rm -rf ./tmp/foo 31 | mkdir -p ./tmp/foo 32 | cp ./tmp/lua.nix ./tmp/foo/default.nix 33 | # now ./tmp/foo is a directory containing ./tmp/foo/default.nix (again) 34 | 35 | run cached-nix-shell ./tmp/foo --run 'lua -v' 36 | check_contains "Lua.org" 37 | check_fast 38 | -------------------------------------------------------------------------------- /tests/t08-args.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . ./lib.sh 3 | 4 | # Check that changing --run/--exec arguments do not invalidate the cache. 5 | run cached-nix-shell -p lua --run 'lua -v' 6 | check_contains "Lua.org" 7 | check_slow 8 | 9 | run cached-nix-shell -p lua --run 'lua -v | rev' 10 | check_contains "gro.auL" 11 | check_fast 12 | 13 | run cached-nix-shell -p luajit --run 'lua -v' 14 | check_contains "https\?://luajit.org/" 15 | check_slow 16 | 17 | run cached-nix-shell -p luajit --exec lua -v 18 | check_contains "https\?://luajit.org/" 19 | check_fast 20 | 21 | 22 | # Check argument expanding "-pj16" -> "-p -j 16" 23 | run cached-nix-shell -pj16 luajit --exec lua -v 24 | check_contains "https\?://luajit.org/" 25 | check_fast 26 | 27 | 28 | # Check -v/--verbose. 29 | # This test produces a lot of output, so, pipe it through `tail` 30 | run cached-nix-shell -vp --run : | tail -n3 31 | check_slow 32 | check_stderr_contains "^evaluating file '/" 33 | 34 | run cached-nix-shell -vp --run : 35 | check_fast 36 | 37 | 38 | # Check shebang argument passing. 39 | run_inline a b c << 'EOF' 40 | #!/usr/bin/env cached-nix-shell 41 | #! nix-shell -i bash -p 42 | 43 | printf "count=%s" "$#" 44 | printf " '%s'" "$@" 45 | printf "\n" 46 | EOF 47 | check_contains "^count=3 'a' 'b' 'c'$" 48 | 49 | 50 | # Check nontrivial interpreter. 51 | run_inline a 'b c' << 'EOF' 52 | #!/usr/bin/env cached-nix-shell 53 | #! nix-shell -i "printf ', %q' {1..3}" -p 54 | EOF 55 | check_contains "^, 1, 2, 3, \./tmp/inline[0-9]\+, a, 'b c'$" 56 | -------------------------------------------------------------------------------- /tests/t09-I.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . ./lib.sh 3 | 4 | run_inline << 'EOF' 5 | #! /usr/bin/env cached-nix-shell 6 | --[[ 7 | #! nix-shell -i lua -p "luajit.withPackages (p: [ p.basexx ] )" 8 | #! nix-shell -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/nixos-22.11.tar.gz 9 | --]] 10 | 11 | print(require("basexx").to_base64("hello")) 12 | EOF 13 | check_contains "aGVsbG8=" 14 | -------------------------------------------------------------------------------- /tests/t10-non-utf8.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . ./lib.sh 3 | 4 | unset F0 F1 5 | F0="$(printf '%b' '\360')" 6 | F1="$(printf '%b' '\361')" 7 | 8 | 9 | # Non-UTF-8 PWD 10 | # Skip on filesystems that do not support non-UTF-8 paths, e.g. APFS. 11 | if mkdir ./tmp/a"$F0"b; then 12 | put ./tmp/a"$F0"b/shell.nix << 'EOF' 13 | with import { }; mkShell { buildInputs = [ lua ]; } 14 | EOF 15 | 16 | put +x ./tmp/a"$F0"b/run.sh << 'EOF' 17 | #!/usr/bin/env cached-nix-shell 18 | #! nix-shell -i sh --pure ./shell.nix 19 | lua -v 20 | EOF 21 | 22 | run cached-nix-shell ./tmp/a"$F0"b/run.sh 23 | check_contains "Lua.org" 24 | fi 25 | 26 | 27 | # Non-UTF-8 environment variable passed to shebang script 28 | export VAR=a"$F0"b 29 | run_inline << 'EOF' 30 | #!/usr/bin/env cached-nix-shell 31 | #! nix-shell -pi sh 32 | env -0 | LANG=C grep -az "^VAR=" | cat -v 33 | EOF 34 | check_contains '^VAR=aM-pb\^@$' 35 | unset VAR 36 | 37 | 38 | # Non-UTF-8 shebang script 39 | run_inline << EOF # unquoted 40 | #!/usr/bin/env cached-nix-shell 41 | #! nix-shell -pi sh 42 | echo '$F0$F1' | cat -v 43 | EOF 44 | check_contains '^M-pM-q$' 45 | 46 | 47 | # Non-UTF-8 shebang script argument 48 | run_inline foo"$F0"bar << 'EOF' 49 | #!/usr/bin/env cached-nix-shell 50 | #! nix-shell -pi sh 51 | printf "!%s!\n" "$@" | cat -v 52 | EOF 53 | check_contains "^!fooM-pbar!$" 54 | 55 | 56 | # Non-UTF-8 environment variable passed to/exported by setup.sh 57 | export VAR_IN=a"$F0"b 58 | put ./tmp/shellhook.nix << 'EOF' 59 | with import { }; 60 | mkShell { 61 | shellHook = '' 62 | export VAR_OUT 63 | VAR_OUT=out:$(env -0 | LANG=C grep -z "^VAR_IN=" | cat -v) 64 | ''; 65 | } 66 | EOF 67 | run cached-nix-shell ./tmp/shellhook.nix --pure --keep VAR_IN \ 68 | --run 'printf "%s\n" "$VAR_OUT"' 69 | check_contains '^out:VAR_IN=aM-pb\^@$' 70 | unset VAR_IN 71 | 72 | 73 | # Non-UTF-8 evaluation result 74 | put ./tmp/evaluation.nix << EOF # unquoted 75 | with import { }; 76 | mkShell { 77 | "VAR1" = "A${F0}B"; 78 | "VAR${F1}2" = "CD"; 79 | } 80 | EOF 81 | 82 | # Skip because nix show-derivation fails 83 | run cached-nix-shell ./tmp/evaluation.nix --pure \ 84 | --run 'env -0 | LANG=C grep -z "^VAR1=" | cat -v' 85 | skip true check_contains '^VAR1=AM-pB\^@$' 86 | 87 | run cached-nix-shell ./tmp/evaluation.nix --pure \ 88 | --run 'env -0 | LANG=C grep -z "^VAR.2=" | cat -v' 89 | skip true check_contains '^VARM-q2=CD\^@$' 90 | -------------------------------------------------------------------------------- /tests/t11-wrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . ./lib.sh 3 | # Test --wrap 4 | 5 | run cached-nix-shell --wrap env nix-shell -p lua --run 'lua -v' 6 | check_contains "Lua.org" 7 | check_slow 8 | 9 | run cached-nix-shell --wrap env nix-shell -p lua --run 'lua -v' 10 | check_contains "Lua.org" 11 | check_fast 12 | -------------------------------------------------------------------------------- /tests/t12-shellhook-output.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . ./lib.sh 3 | # Ensure that the output of shellHook does not screw the environment variables. 4 | 5 | put ./tmp/shell.nix << 'EOF' 6 | with import { }; mkShell { shellHook = "echo hello$((6*7))world"; } 7 | EOF 8 | 9 | run cached-nix-shell ./tmp/shell.nix --run 'env > tmp/env' 10 | 11 | check_contains hello42world 12 | 13 | check "tmp/env does not contain hello42world" \ 14 | not grep -q hello42world tmp/env 15 | -------------------------------------------------------------------------------- /tests/t13-normalize-pwd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . ./lib.sh 3 | # Check the normalization of PWD. The cache should evaluate in the directory 4 | # containing default.nix no matter the current PWD or the way the path to 5 | # default.nix is specified. This behavior doesn't match the behavior of 6 | # nix-shell, but this test case checks that at least it is consistent. 7 | 8 | mkdir tmp/foo tmp/bar 9 | put +x ./tmp/foo/default.nix << 'EOF' 10 | with import { }; mkShell { 11 | shellHook = ''printf '%s\n' "$PWD" > "$GOT"''; 12 | } 13 | EOF 14 | 15 | check_normalized_pwd() { 16 | rm -f "$GOT" tmp/cache/cached-nix-shell/* 17 | run --chdir "$1" cached-nix-shell --keep GOT "$2" --run : 18 | check "got $(cat tmp/got)" cmp -s tmp/expected tmp/got 19 | } 20 | 21 | export GOT=$PWD/tmp/got 22 | printf "%s\n" "$PWD/tmp/foo" > "$PWD/tmp/expected" 23 | printf "Expected: %s\n" "$PWD/tmp/foo" 24 | 25 | check_normalized_pwd . tmp/foo 26 | check_normalized_pwd . tmp/foo/ 27 | check_normalized_pwd . tmp/foo/default.nix 28 | 29 | check_normalized_pwd . ./tmp/foo 30 | check_normalized_pwd . ./tmp/foo/ 31 | check_normalized_pwd . ./tmp/foo/default.nix 32 | 33 | check_normalized_pwd . "$PWD"/tmp/foo 34 | check_normalized_pwd . "$PWD"/tmp/foo/ 35 | check_normalized_pwd . "$PWD"/tmp/foo/default.nix 36 | 37 | check_normalized_pwd tmp/foo "" 38 | check_normalized_pwd tmp/foo . 39 | check_normalized_pwd tmp/foo ./default.nix 40 | check_normalized_pwd tmp/foo default.nix 41 | 42 | check_normalized_pwd tmp/bar ../foo 43 | check_normalized_pwd tmp/bar ../foo/ 44 | check_normalized_pwd tmp/bar ../foo/default.nix 45 | -------------------------------------------------------------------------------- /tests/t14-special-paths.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . ./lib.sh 3 | 4 | put ./tmp/foo << 'EOF' 5 | with import { }; 6 | mkShell { VAR="val"; } 7 | EOF 8 | 9 | put ./tmp/empty << 'EOF' 10 | {} 11 | EOF 12 | 13 | # Get default NIX_PATH in case it's not set. Do not export it to not interfere 14 | # with tests that do not use it. 15 | [ "${NIX_PATH:-}" ] || NIX_PATH="$( 16 | nix-instantiate --eval -E ' 17 | builtins.concatStringsSep ":" 18 | (map (a: (if a.prefix == "" then "" else a.prefix + "=") + a.path) 19 | builtins.nixPath) 20 | ' | tr -d '"')" 21 | 22 | ################################################################################ 23 | # 24 | ################################################################################ 25 | 26 | # , absolute NIX_PATH 27 | 28 | run env "NIX_PATH=$PWD/tmp:$NIX_PATH" cached-nix-shell '' --run 'echo $VAR' 29 | check_contains "^val$" 30 | check_slow 31 | 32 | run --chdir .. env "NIX_PATH=$PWD/tmp:$NIX_PATH" cached-nix-shell '' --run 'echo $VAR' 33 | check_contains "^val$" 34 | check_fast 35 | 36 | run env "NIX_PATH=bar=$PWD/tmp/foo:$NIX_PATH" cached-nix-shell '' --run 'echo $VAR' 37 | check_contains "^val$" 38 | check_slow 39 | 40 | run --chdir .. env "NIX_PATH=bar=$PWD/tmp/foo:$NIX_PATH" cached-nix-shell '' --run 'echo $VAR' 41 | check_contains "^val$" 42 | check_fast 43 | 44 | # , absolute -I 45 | 46 | run cached-nix-shell -I "$PWD/tmp" '' --run 'echo $VAR' 47 | check_contains "^val$" 48 | check_slow 49 | 50 | run --chdir .. cached-nix-shell -I "$PWD/tmp" '' --run 'echo $VAR' 51 | check_contains "^val$" 52 | check_fast 53 | 54 | run cached-nix-shell -I "bar=$PWD/tmp/foo" '' --run 'echo $VAR' 55 | check_contains "^val$" 56 | check_slow 57 | 58 | run --chdir .. cached-nix-shell -I "bar=$PWD/tmp/foo" '' --run 'echo $VAR' 59 | check_contains "^val$" 60 | check_fast 61 | 62 | # , relative NIX_PATH 63 | 64 | run env "NIX_PATH=.:$NIX_PATH" cached-nix-shell '' --run 'echo $VAR' 65 | check_contains "^val$" 66 | 67 | run env "NIX_PATH=bar=./tmp:$NIX_PATH" cached-nix-shell '' --run 'echo $VAR' 68 | check_contains "^val$" 69 | 70 | # , relative -I 71 | 72 | run cached-nix-shell -I . '' --run 'echo $VAR' 73 | check_contains "^val$" 74 | 75 | run cached-nix-shell -I bar=./tmp/foo '' --run 'echo $VAR' 76 | check_contains "^val$" 77 | 78 | ################################################################################ 79 | # Other cases 80 | ################################################################################ 81 | 82 | # URI 83 | run cached-nix-shell https://github.com/NixOS/nixpkgs/archive/nixos-22.11.tar.gz -A lua --run 'env' 84 | check_contains '^name=lua-5\..*' 85 | 86 | # Multiple .nix files (why it is even supported by nix-shell) 87 | 88 | run cached-nix-shell ./tmp/foo ./tmp/empty --run 'echo $VAR' 89 | check_contains "^val$" 90 | 91 | run cached-nix-shell ./tmp/empty ./tmp/foo --run 'echo $VAR' 92 | check_contains "^val$" 93 | 94 | # Path to .drv file 95 | 96 | lua_drv=$(nix-instantiate --no-gc-warning -E '(import {}).lua') 97 | 98 | run cached-nix-shell "$lua_drv" --run 'env' 99 | check_contains '^name=lua-5\..*' 100 | 101 | # TODO: `nix-shell /nix/store/*.drv!out` is not implemented yet. 102 | # run cached-nix-shell "$lua_drv!out" --run 'env' 103 | # check_contains '^name=lua-5\..*' 104 | -------------------------------------------------------------------------------- /tests/t15-old-nix.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . ./lib.sh 3 | # Check that cached-nix-shell works even if the $PATH contains an old derivation 4 | # of nix-shell. https://github.com/xzfc/cached-nix-shell/issues/24 5 | 6 | PATH=$(nix-build '' -A nix --no-out-link -I \ 7 | old=https://github.com/NixOS/nixpkgs/archive/nixos-21.05.tar.gz 8 | )/bin:$PATH run cached-nix-shell -p --exec nix-shell --version 9 | check_contains '^nix-shell (Nix) 2\.3\.16$' 10 | -------------------------------------------------------------------------------- /tests/t16-shopt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . ./lib.sh 3 | # Check that shell options are preserved. 4 | # https://github.com/xzfc/cached-nix-shell/issues/28 5 | 6 | run nix-shell --pure -p --run '{ shopt -p; shopt -po; } > tmp/nix-shell' 7 | run cached-nix-shell --pure -p --run '{ shopt -p; shopt -po; } > tmp/cached-nix-shell' 8 | check "Options are the same" diff tmp/nix-shell tmp/cached-nix-shell 9 | rm tmp/nix-shell tmp/cached-nix-shell 10 | 11 | run $(nix-shell --pure -p which expect --run 'which expect') << 'EOF' 12 | set timeout 60 13 | log_user 0 14 | 15 | spawn nix-shell --pure -p 16 | send "\{ shopt -p; shopt -po; \} > tmp/nix-shell; exit\r" 17 | expect eof 18 | 19 | spawn cached-nix-shell --pure -p 20 | send "\{ shopt -p; shopt -po; \} > tmp/cached-nix-shell; exit\r" 21 | expect eof 22 | EOF 23 | check "Options are the same" diff tmp/nix-shell tmp/cached-nix-shell 24 | -------------------------------------------------------------------------------- /tests/t17-interactive.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . ./lib.sh 3 | # Test interactive shell 4 | 5 | unset PS1 BASHRC_IS_SOURCED NIX_SHELL_PRESERVE_PROMPT 6 | 7 | expect=$(nix-shell --pure -p which expect --run 'which expect') 8 | put +x tmp/run << EOF 9 | # Make sure that prompt containing long PWD will fit 10 | set stty_init "rows 10 cols 2000" 11 | # TODO: fix test not to download nixpkgs when running on CI 12 | set timeout 180 ; 13 | 14 | log_file -a tmp/out 15 | log_user 0 16 | spawn sh -c [lindex \$argv 0] 17 | send [string map {"\n" "\r\n"} [read stdin]] 18 | send "exit\r" 19 | expect eof 20 | log_file 21 | exec sh -c "tr -d \"\\r\" < tmp/out > tmp/err" 22 | EOF 23 | 24 | mkdir -p tmp/home 25 | export HOME=$PWD/tmp/home 26 | put tmp/home/.bashrc << 'EOF' 27 | PS1=NEW_PROMPT 28 | BASHRC_IS_SOURCED=1 29 | EOF 30 | 31 | run $expect tmp/run 'sleep 0.1 && cached-nix-shell --pure -p' << 'EOF' 32 | echo uryyb jbeyq | tr a-z n-za-m 33 | EOF 34 | check_contains '\[cached-nix-shell:' 35 | check_contains 'hello world' 36 | check_slow 37 | 38 | run $expect tmp/run 'cached-nix-shell --pure -p' << 'EOF' 39 | echo uryyb jbeyq | tr a-z n-za-m 40 | EOF 41 | check_contains 'hello world' 42 | skip '[ "$(uname)" = Darwin ]' check_fast # TODO: fix /tmp/nix-$$-* issue 43 | 44 | run cached-nix-shell --pure -p --exec true 45 | check_fast 46 | 47 | 48 | # Check pre-defined env vars and inclusion of .bashrc 49 | run $expect tmp/run 'cached-nix-shell --pure -p' << 'EOF' 50 | echo IN_NIX_SHELL=$IN_NIX_SHELL 51 | echo IN_CACHED_NIX_SHELL=$IN_CACHED_NIX_SHELL 52 | echo BASHRC_IS_SOURCED=${BASHRC_IS_SOURCED:-unset} 53 | echo PS1=$PS1 54 | EOF 55 | check_contains 'IN_NIX_SHELL=pure' 56 | check_contains 'IN_CACHED_NIX_SHELL=1' 57 | check_contains 'BASHRC_IS_SOURCED=unset' 58 | check_contains 'PS1=.*\[cached-nix-shell:\\w].*' 59 | check_fast 60 | 61 | run $expect tmp/run 'cached-nix-shell -p' << 'EOF' 62 | echo IN_NIX_SHELL=$IN_NIX_SHELL 63 | echo IN_CACHED_NIX_SHELL=$IN_CACHED_NIX_SHELL 64 | echo BASHRC_IS_SOURCED=${BASHRC_IS_SOURCED:-unset} 65 | echo PS1=$PS1 66 | EOF 67 | check_contains 'IN_NIX_SHELL=impure' 68 | check_contains 'IN_CACHED_NIX_SHELL=1' 69 | check_contains 'BASHRC_IS_SOURCED=1' 70 | check_contains 'PS1=.*\[cached-nix-shell:\\w].*' 71 | check_fast 72 | 73 | NIX_SHELL_PRESERVE_PROMPT=1 run $expect tmp/run 'cached-nix-shell -p' << 'EOF' 74 | echo PS1=$PS1 75 | EOF 76 | check_contains 'PS1=NEW_PROMPT' 77 | check_fast 78 | -------------------------------------------------------------------------------- /tests/t18-invalidate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . ./lib.sh 3 | # Check that the cache is invalidated when a dependency is deleted. 4 | 5 | put tmp/shell.nix << 'EOF' 6 | with import { }; 7 | let 8 | dep = stdenv.mkDerivation { 9 | name = "cached-nix-shell-test-inner-dep"; 10 | unpackPhase = ": | md5sum | cut -c 1-32 > $out"; 11 | }; 12 | in mkShell { inherit dep; } 13 | EOF 14 | 15 | run cached-nix-shell tmp/shell.nix --pure --run 'cat $dep; echo $dep > tmp/dep' 16 | check_contains d41d8cd98f00b204e9800998ecf8427e 17 | check_slow 18 | 19 | run cached-nix-shell tmp/shell.nix --pure --run 'cat $dep' 20 | check_contains d41d8cd98f00b204e9800998ecf8427e 21 | check_fast 22 | 23 | run nix-store --delete $(cat tmp/dep) 24 | 25 | run cached-nix-shell tmp/shell.nix --pure --run 'cat $dep' 26 | check_contains d41d8cd98f00b204e9800998ecf8427e 27 | check_slow 28 | -------------------------------------------------------------------------------- /tests/t19-tarball-unpack.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . ./lib.sh 3 | # Check that tarball unpacking wont interfere with the cache. 4 | 5 | mkdir -p tmp/repo/data 6 | echo '(import {}).hello' > tmp/repo/data/default.nix 7 | tar -C tmp/repo -cf tmp/repo.tar data 8 | 9 | R=$$_$(date +%s) 10 | 11 | run cached-nix-shell -I q="file://$PWD/tmp/repo.tar?a$R" -p 'import ' --run : 12 | check_slow 13 | 14 | run cached-nix-shell -I q="file://$PWD/tmp/repo.tar?a$R" -p 'import ' --run : 15 | skip '[ "$(uname)" = Darwin ]' check_fast 16 | 17 | run cached-nix-shell -p "fetchTarball file://$PWD/tmp/repo.tar?b$R" --run : 18 | check_slow 19 | 20 | run cached-nix-shell -p "fetchTarball file://$PWD/tmp/repo.tar?b$R" --run : 21 | skip '[ "$(uname)" = Darwin ]' check_fast 22 | --------------------------------------------------------------------------------