diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a9ca74c17cc..763c5f27ee6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,16 +11,7 @@ .github/CODEOWNERS @edolstra # Documentation of built-in functions -src/libexpr/primops.cc @roberth @fricklerhandwerk - -# Documentation of settings -src/libexpr/eval-settings.hh @fricklerhandwerk -src/libstore/globals.hh @fricklerhandwerk - -# Documentation -doc/manual @fricklerhandwerk -maintainers/*.md @fricklerhandwerk -src/**/*.md @fricklerhandwerk +src/libexpr/primops.cc @roberth # Libstore layer /src/libstore @ericson2314 diff --git a/.github/STALE-BOT.md b/.github/STALE-BOT.md index 383717bfc1d..bc0005413f1 100644 --- a/.github/STALE-BOT.md +++ b/.github/STALE-BOT.md @@ -3,7 +3,7 @@ - Thanks for your contribution! - To remove the stale label, just leave a new comment. - _How to find the right people to ping?_ → [`git blame`](https://git-scm.com/docs/git-blame) to the rescue! (or GitHub's history and blame buttons.) -- You can always ask for help on [our Discourse Forum](https://discourse.nixos.org/) or on [Matrix - #nix:nixos.org](https://matrix.to/#/#nix:nixos.org). +- You can always ask for help on [our Discourse Forum](https://discourse.nixos.org/) or on [Matrix - #users:nixos.org](https://matrix.to/#/#users:nixos.org). ## Suggestions for PRs diff --git a/.github/actions/install-nix-action/action.yaml b/.github/actions/install-nix-action/action.yaml new file mode 100644 index 00000000000..28103f5891c --- /dev/null +++ b/.github/actions/install-nix-action/action.yaml @@ -0,0 +1,50 @@ +name: "Install Nix" +description: "Helper action for installing Nix with support for dogfooding from master" +inputs: + dogfood: + description: "Whether to use Nix installed from the latest artifact from master branch" + required: true # Be explicit about the fact that we are using unreleased artifacts + extra_nix_config: + description: "Gets appended to `/etc/nix/nix.conf` if passed." + install_url: + description: "URL of the Nix installer" + required: false + default: "https://releases.nixos.org/nix/nix-2.30.1/install" + github_token: + description: "Github token" + required: true +runs: + using: "composite" + steps: + - name: "Download nix install artifact from master" + shell: bash + id: download-nix-installer + if: ${{ inputs.dogfood }} + run: | + RUN_ID=$(gh run list --repo "$DOGFOOD_REPO" --workflow ci.yml --branch master --status success --json databaseId --jq ".[0].databaseId") + + if [ "$RUNNER_OS" == "Linux" ]; then + INSTALLER_ARTIFACT="installer-linux" + elif [ "$RUNNER_OS" == "macOS" ]; then + INSTALLER_ARTIFACT="installer-darwin" + else + echo "::error ::Unsupported RUNNER_OS: $RUNNER_OS" + exit 1 + fi + + INSTALLER_DOWNLOAD_DIR="$GITHUB_WORKSPACE/$INSTALLER_ARTIFACT" + mkdir -p "$INSTALLER_DOWNLOAD_DIR" + + gh run download "$RUN_ID" --repo "$DOGFOOD_REPO" -n "$INSTALLER_ARTIFACT" -D "$INSTALLER_DOWNLOAD_DIR" + echo "installer-path=file://$INSTALLER_DOWNLOAD_DIR" >> "$GITHUB_OUTPUT" + + echo "::notice ::Dogfooding Nix installer from master (https://github.com/$DOGFOOD_REPO/actions/runs/$RUN_ID)" + env: + GH_TOKEN: ${{ inputs.github_token }} + DOGFOOD_REPO: "NixOS/nix" + - uses: cachix/install-nix-action@c134e4c9e34bac6cab09cf239815f9339aaaf84e # v31.5.1 + with: + # Ternary operator in GHA: https://www.github.com/actions/runner/issues/409#issuecomment-752775072 + install_url: ${{ inputs.dogfood && format('{0}/install', steps.download-nix-installer.outputs.installer-path) || inputs.install_url }} + install_options: ${{ inputs.dogfood && format('--tarball-url-prefix {0}', steps.download-nix-installer.outputs.installer-path) || '' }} + extra_nix_config: ${{ inputs.extra_nix_config }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be96bb48469..da6f3590771 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,154 +7,142 @@ on: permissions: read-all jobs: + eval: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: ./.github/actions/install-nix-action + with: + dogfood: true + extra_nix_config: + experimental-features = nix-command flakes + github_token: ${{ secrets.GITHUB_TOKEN }} + - run: nix flake show --all-systems --json tests: - needs: [check_secrets] strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] - runs-on: ${{ matrix.os }} + include: + - scenario: on ubuntu + runs-on: ubuntu-24.04 + os: linux + - scenario: on macos + runs-on: macos-14 + os: darwin + name: tests ${{ matrix.scenario }} + runs-on: ${{ matrix.runs-on }} timeout-minutes: 60 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: cachix/install-nix-action@v30 + - uses: ./.github/actions/install-nix-action with: + github_token: ${{ secrets.GITHUB_TOKEN }} + dogfood: true # The sandbox would otherwise be disabled by default on Darwin extra_nix_config: | sandbox = true max-jobs = 1 - - run: echo CACHIX_NAME="$(echo $GITHUB_REPOSITORY-install-tests | tr "[A-Z]/" "[a-z]-")" >> $GITHUB_ENV - - uses: cachix/cachix-action@v15 - if: needs.check_secrets.outputs.cachix == 'true' + - uses: DeterminateSystems/magic-nix-cache-action@main + # Since ubuntu 22.30, unprivileged usernamespaces are no longer allowed to map to the root user: + # https://ubuntu.com/blog/ubuntu-23-10-restricted-unprivileged-user-namespaces + - run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + if: matrix.os == 'linux' + - run: scripts/build-checks + - run: scripts/prepare-installer-for-github-actions + - name: Upload installer tarball + uses: actions/upload-artifact@v4 with: - name: '${{ env.CACHIX_NAME }}' - signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}' - authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' - - if: matrix.os == 'ubuntu-latest' - run: | - free -h - swapon --show - swap=$(swapon --show --noheadings | head -n 1 | awk '{print $1}') - echo "Found swap: $swap" - sudo swapoff $swap - # resize it (fallocate) - sudo fallocate -l 10G $swap - sudo mkswap $swap - sudo swapon $swap - free -h - ( - while sleep 60; do - free -h - done - ) & - - run: nix --experimental-features 'nix-command flakes' flake check -L - - run: nix --experimental-features 'nix-command flakes' flake show --all-systems --json + name: installer-${{matrix.os}} + path: out/* + + installer_test: + needs: [tests] + strategy: + fail-fast: false + matrix: + include: + - scenario: on ubuntu + runs-on: ubuntu-24.04 + os: linux + - scenario: on macos + runs-on: macos-14 + os: darwin + name: installer test ${{ matrix.scenario }} + runs-on: ${{ matrix.runs-on }} + steps: + - uses: actions/checkout@v4 + - name: Download installer tarball + uses: actions/download-artifact@v4 + with: + name: installer-${{matrix.os}} + path: out + - name: Serving installer + id: serving_installer + run: ./scripts/serve-installer-for-github-actions + - uses: cachix/install-nix-action@v31 + with: + install_url: 'http://localhost:8126/install' + install_options: "--tarball-url-prefix http://localhost:8126/" + - run: sudo apt install fish zsh + if: matrix.os == 'linux' + - run: brew install fish + if: matrix.os == 'darwin' + - run: exec bash -c "nix-instantiate -E 'builtins.currentTime' --eval" + - run: exec sh -c "nix-instantiate -E 'builtins.currentTime' --eval" + - run: exec zsh -c "nix-instantiate -E 'builtins.currentTime' --eval" + - run: exec fish -c "nix-instantiate -E 'builtins.currentTime' --eval" + - run: exec bash -c "nix-channel --add https://releases.nixos.org/nixos/unstable/nixos-23.05pre466020.60c1d71f2ba nixpkgs" + - run: exec bash -c "nix-channel --update && nix-env -iA nixpkgs.hello && hello" # Steps to test CI automation in your own fork. - # Cachix: - # 1. Sign-up for https://www.cachix.org/ - # 2. Create a cache for $githubuser-nix-install-tests - # 3. Create a cachix auth token and save it in https://github.com/$githubuser/nix/settings/secrets/actions in "Repository secrets" as CACHIX_AUTH_TOKEN - # Dockerhub: # 1. Sign-up for https://hub.docker.com/ # 2. Store your dockerhub username as DOCKERHUB_USERNAME in "Repository secrets" of your fork repository settings (https://github.com/$githubuser/nix/settings/secrets/actions) # 3. Create an access token in https://hub.docker.com/settings/security and store it as DOCKERHUB_TOKEN in "Repository secrets" of your fork check_secrets: permissions: contents: none - name: Check Cachix and Docker secrets present for installer tests - runs-on: ubuntu-latest + name: Check Docker secrets present for installer tests + runs-on: ubuntu-24.04 outputs: - cachix: ${{ steps.secret.outputs.cachix }} docker: ${{ steps.secret.outputs.docker }} steps: - name: Check for secrets id: secret env: - _CACHIX_SECRETS: ${{ secrets.CACHIX_SIGNING_KEY }}${{ secrets.CACHIX_AUTH_TOKEN }} _DOCKER_SECRETS: ${{ secrets.DOCKERHUB_USERNAME }}${{ secrets.DOCKERHUB_TOKEN }} run: | - echo "::set-output name=cachix::${{ env._CACHIX_SECRETS != '' }}" echo "::set-output name=docker::${{ env._DOCKER_SECRETS != '' }}" - installer: - needs: [tests, check_secrets] - if: github.event_name == 'push' && needs.check_secrets.outputs.cachix == 'true' - runs-on: ubuntu-latest - outputs: - installerURL: ${{ steps.prepare-installer.outputs.installerURL }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - run: echo CACHIX_NAME="$(echo $GITHUB_REPOSITORY-install-tests | tr "[A-Z]/" "[a-z]-")" >> $GITHUB_ENV - - uses: cachix/install-nix-action@v30 - with: - install_url: https://releases.nixos.org/nix/nix-2.20.3/install - - uses: cachix/cachix-action@v15 - with: - name: '${{ env.CACHIX_NAME }}' - signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}' - authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' - cachixArgs: '-v' - - id: prepare-installer - run: scripts/prepare-installer-for-github-actions - - installer_test: - needs: [installer, check_secrets] - if: github.event_name == 'push' && needs.check_secrets.outputs.cachix == 'true' - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - run: echo CACHIX_NAME="$(echo $GITHUB_REPOSITORY-install-tests | tr "[A-Z]/" "[a-z]-")" >> $GITHUB_ENV - - uses: cachix/install-nix-action@v30 - with: - install_url: '${{needs.installer.outputs.installerURL}}' - install_options: "--tarball-url-prefix https://${{ env.CACHIX_NAME }}.cachix.org/serve" - - run: sudo apt install fish zsh - if: matrix.os == 'ubuntu-latest' - - run: brew install fish - if: matrix.os == 'macos-latest' - - run: exec bash -c "nix-instantiate -E 'builtins.currentTime' --eval" - - run: exec sh -c "nix-instantiate -E 'builtins.currentTime' --eval" - - run: exec zsh -c "nix-instantiate -E 'builtins.currentTime' --eval" - - run: exec fish -c "nix-instantiate -E 'builtins.currentTime' --eval" - - run: exec bash -c "nix-channel --add https://releases.nixos.org/nixos/unstable/nixos-23.05pre466020.60c1d71f2ba nixpkgs" - - run: exec bash -c "nix-channel --update && nix-env -iA nixpkgs.hello && hello" - docker_push_image: - needs: [check_secrets, tests, vm_tests] + needs: [tests, vm_tests, check_secrets] permissions: contents: read packages: write if: >- + needs.check_secrets.outputs.docker == 'true' && github.event_name == 'push' && - github.ref_name == 'master' && - needs.check_secrets.outputs.cachix == 'true' && - needs.check_secrets.outputs.docker == 'true' - runs-on: ubuntu-latest + github.ref_name == 'master' + runs-on: ubuntu-24.04 steps: + - name: Check for secrets + id: secret + env: + _DOCKER_SECRETS: ${{ secrets.DOCKERHUB_USERNAME }}${{ secrets.DOCKERHUB_TOKEN }} + run: | + echo "::set-output name=docker::${{ env._DOCKER_SECRETS != '' }}" - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: install_url: https://releases.nixos.org/nix/nix-2.20.3/install - - run: echo CACHIX_NAME="$(echo $GITHUB_REPOSITORY-install-tests | tr "[A-Z]/" "[a-z]-")" >> $GITHUB_ENV + - uses: DeterminateSystems/magic-nix-cache-action@main - run: echo NIX_VERSION="$(nix --experimental-features 'nix-command flakes' eval .\#nix.version | tr -d \")" >> $GITHUB_ENV - - uses: cachix/cachix-action@v15 - if: needs.check_secrets.outputs.cachix == 'true' - with: - name: '${{ env.CACHIX_NAME }}' - signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}' - authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' - run: nix --experimental-features 'nix-command flakes' build .#dockerImage -L - run: docker load -i ./result/image.tar.gz - run: docker tag nix:$NIX_VERSION ${{ secrets.DOCKERHUB_USERNAME }}/nix:$NIX_VERSION @@ -191,10 +179,15 @@ jobs: docker push $IMAGE_ID:master vm_tests: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - - uses: DeterminateSystems/nix-installer-action@main + - uses: ./.github/actions/install-nix-action + with: + dogfood: true + extra_nix_config: + experimental-features = nix-command flakes + github_token: ${{ secrets.GITHUB_TOKEN }} - uses: DeterminateSystems/magic-nix-cache-action@main - run: | nix build -L \ @@ -206,7 +199,7 @@ jobs: flake_regressions: needs: vm_tests - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout nix uses: actions/checkout@v4 @@ -220,6 +213,11 @@ jobs: with: repository: NixOS/flake-regressions-data path: flake-regressions/tests - - uses: DeterminateSystems/nix-installer-action@main + - uses: ./.github/actions/install-nix-action + with: + dogfood: true + extra_nix_config: + experimental-features = nix-command flakes + github_token: ${{ secrets.GITHUB_TOKEN }} - uses: DeterminateSystems/magic-nix-cache-action@main - run: nix build -L --out-link ./new-nix && PATH=$(pwd)/new-nix/bin:$PATH MAX_FLAKES=25 flake-regressions/eval-all.sh diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 34aa4e6bdf0..23a5d9e51fc 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -15,7 +15,7 @@ permissions: jobs: labels: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 if: github.repository_owner == 'NixOS' steps: - uses: actions/labeler@v5 diff --git a/.gitignore b/.gitignore index de1183977b3..4782bfbafd2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,110 +1,12 @@ -Makefile.config -perl/Makefile.config - -# / -/aclocal.m4 -/autom4te.cache -/precompiled-headers.h.gch -/config.* -/configure -/stamp-h1 -/svn-revision -/libtool -/config/config.* # Default meson build dir /build -# /doc/manual/ -/doc/manual/*.1 -/doc/manual/*.5 -/doc/manual/*.8 -/doc/manual/generated/* -/doc/manual/nix.json -/doc/manual/conf-file.json -/doc/manual/language.json -/doc/manual/xp-features.json -/doc/manual/source/SUMMARY.md -/doc/manual/source/SUMMARY-rl-next.md -/doc/manual/source/store/types/* -!/doc/manual/source/store/types/index.md.in -/doc/manual/source/command-ref/new-cli -/doc/manual/source/command-ref/conf-file.md -/doc/manual/source/command-ref/experimental-features-shortlist.md -/doc/manual/source/contributing/experimental-feature-descriptions.md -/doc/manual/source/language/builtins.md -/doc/manual/source/language/builtin-constants.md -/doc/manual/source/release-notes/rl-next.md - -# /scripts/ -/scripts/nix-profile.sh -/scripts/nix-profile-daemon.sh -/scripts/nix-profile.fish -/scripts/nix-profile-daemon.fish - -# /src/libexpr/ -/src/libexpr/lexer-tab.cc -/src/libexpr/lexer-tab.hh -/src/libexpr/parser-tab.cc -/src/libexpr/parser-tab.hh -/src/libexpr/parser-tab.output -/src/libexpr/nix.tbl -/src/libexpr/tests -/src/libexpr-tests/libnixexpr-tests - -# /src/libfetchers -/src/libfetchers-tests/libnixfetchers-tests - -# /src/libflake -/src/libflake-tests/libnixflake-tests - -# /src/libstore/ -*.gen.* -/src/libstore/tests -/src/libstore-tests/libnixstore-tests - -# /src/libutil/ -/src/libutil/tests -/src/libutil-tests/libnixutil-tests - -/src/nix/nix - -/src/nix/generated-doc - -# /src/nix-env/ -/src/nix-env/nix-env - -# /src/nix-instantiate/ -/src/nix-instantiate/nix-instantiate - -# /src/nix-store/ -/src/nix-store/nix-store - -/src/nix-prefetch-url/nix-prefetch-url - -/src/nix-collect-garbage/nix-collect-garbage - -# /src/nix-channel/ -/src/nix-channel/nix-channel - -# /src/nix-build/ -/src/nix-build/nix-build - -/src/nix-copy-closure/nix-copy-closure - -/src/error-demo/error-demo - -/src/build-remote/build-remote - # /tests/functional/ -/tests/functional/test-tmp /tests/functional/common/subst-vars.sh -/tests/functional/result* /tests/functional/restricted-innocent -/tests/functional/shell -/tests/functional/shell.drv -/tests/functional/repl-result-out /tests/functional/debugger-test-out /tests/functional/test-libstoreconsumer/test-libstoreconsumer +/tests/functional/nix-shell # /tests/functional/lang/ /tests/functional/lang/*.out @@ -112,27 +14,9 @@ perl/Makefile.config /tests/functional/lang/*.err /tests/functional/lang/*.ast -/perl/lib/Nix/Config.pm -/perl/lib/Nix/Store.cc - -/misc/systemd/nix-daemon.service -/misc/systemd/nix-daemon.socket -/misc/systemd/nix-daemon.conf -/misc/upstart/nix-daemon.conf - -outputs/ +/outputs -*.a -*.o -*.o.tmp -*.so -*.dylib -*.dll -*.exe -*.dep *~ -*.pc -*.plist # GNU Global GPATH @@ -147,8 +31,6 @@ GTAGS compile_commands.json *.compile_commands.json -nix-rust/target - result result-* @@ -163,3 +45,8 @@ result-* # Mac OS .DS_Store + +flake-regressions + +# direnv +.direnv/ diff --git a/.mergify.yml b/.mergify.yml index c545bbe6a24..f49144113da 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -2,10 +2,11 @@ queue_rules: - name: default # all required tests need to go here merge_conditions: - - check-success=tests (macos-latest) - - check-success=tests (ubuntu-latest) + - check-success=tests on macos + - check-success=tests on ubuntu + - check-success=installer test on macos + - check-success=installer test on ubuntu - check-success=vm_tests - merge_method: rebase batch_size: 5 pull_request_rules: @@ -26,6 +27,7 @@ pull_request_rules: branches: - 2.18-maintenance labels: + - automatic backport - merge-queue - name: backport patches to 2.19 @@ -36,6 +38,7 @@ pull_request_rules: branches: - 2.19-maintenance labels: + - automatic backport - merge-queue - name: backport patches to 2.20 @@ -46,6 +49,7 @@ pull_request_rules: branches: - 2.20-maintenance labels: + - automatic backport - merge-queue - name: backport patches to 2.21 @@ -56,6 +60,7 @@ pull_request_rules: branches: - 2.21-maintenance labels: + - automatic backport - merge-queue - name: backport patches to 2.22 @@ -66,6 +71,7 @@ pull_request_rules: branches: - 2.22-maintenance labels: + - automatic backport - merge-queue - name: backport patches to 2.23 @@ -76,6 +82,7 @@ pull_request_rules: branches: - 2.23-maintenance labels: + - automatic backport - merge-queue - name: backport patches to 2.24 @@ -86,6 +93,7 @@ pull_request_rules: branches: - "2.24-maintenance" labels: + - automatic backport - merge-queue - name: backport patches to 2.25 @@ -96,4 +104,60 @@ pull_request_rules: branches: - "2.25-maintenance" labels: + - automatic backport + - merge-queue + + - name: backport patches to 2.26 + conditions: + - label=backport 2.26-maintenance + actions: + backport: + branches: + - "2.26-maintenance" + labels: + - automatic backport + - merge-queue + + - name: backport patches to 2.27 + conditions: + - label=backport 2.27-maintenance + actions: + backport: + branches: + - "2.27-maintenance" + labels: + - automatic backport + - merge-queue + + - name: backport patches to 2.28 + conditions: + - label=backport 2.28-maintenance + actions: + backport: + branches: + - "2.28-maintenance" + labels: + - automatic backport + - merge-queue + + - name: backport patches to 2.29 + conditions: + - label=backport 2.29-maintenance + actions: + backport: + branches: + - "2.29-maintenance" + labels: + - automatic backport + - merge-queue + + - name: backport patches to 2.30 + conditions: + - label=backport 2.30-maintenance + actions: + backport: + branches: + - "2.30-maintenance" + labels: + - automatic backport - merge-queue diff --git a/.version b/.version index 7a25c70f90c..bafceb320ec 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.26.0 +2.31.0 diff --git a/README.md b/README.md index 54a6fcc3949..02498944cdb 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Today, a world-wide developer community contributes to Nix and the ecosystem tha - [Nixpkgs](https://github.com/NixOS/nixpkgs) is [the largest, most up-to-date free software repository in the world](https://repology.org/repositories/graphs) - [NixOS](https://github.com/NixOS/nixpkgs/tree/master/nixos) is a Linux distribution that can be configured fully declaratively - [Discourse](https://discourse.nixos.org/) -- [Matrix](https://matrix.to/#/#nix:nixos.org) +- Matrix: [#users:nixos.org](https://matrix.to/#/#users:nixos.org) for user support and [#nix-dev:nixos.org](https://matrix.to/#/#nix-dev:nixos.org) for development ## License diff --git a/default.nix b/default.nix index 2cccff28d51..6466507b714 100644 --- a/default.nix +++ b/default.nix @@ -1,10 +1,9 @@ -(import - ( - let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in - fetchTarball { - url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; - sha256 = lock.nodes.flake-compat.locked.narHash; - } - ) - { src = ./.; } -).defaultNix +(import ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + in + fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } +) { src = ./.; }).defaultNix diff --git a/doc/manual/book.toml b/doc/manual/book.toml.in similarity index 96% rename from doc/manual/book.toml rename to doc/manual/book.toml.in index 213739174b9..34acf642edb 100644 --- a/doc/manual/book.toml +++ b/doc/manual/book.toml.in @@ -1,5 +1,5 @@ [book] -title = "Nix Reference Manual" +title = "Nix @version@ Reference Manual" src = "source" [output.html] diff --git a/doc/manual/generate-builtins.nix b/doc/manual/generate-builtins.nix index 37ed12a4330..3649560f7c6 100644 --- a/doc/manual/generate-builtins.nix +++ b/doc/manual/generate-builtins.nix @@ -5,7 +5,15 @@ in builtinsInfo: let - showBuiltin = name: { doc, type ? null, args ? [ ], experimental-feature ? null, impure-only ? false }: + showBuiltin = + name: + { + doc, + type ? null, + args ? [ ], + experimental-feature ? null, + impure-only ? false, + }: let type' = optionalString (type != null) " (${type})"; diff --git a/doc/manual/generate-deps.py b/doc/manual/generate-deps.py index 297bd39390e..8d6b5a2678f 100755 --- a/doc/manual/generate-deps.py +++ b/doc/manual/generate-deps.py @@ -14,7 +14,7 @@ # literally. since the rules for these aren't even the same for # all three we will just fail when we encounter any of them (if # asserts are off for some reason the depfile will likely point -# to nonexistant paths, making everything phony and thus fine.) +# to nonexistent paths, making everything phony and thus fine.) for path in glob.glob(sys.argv[1] + '/**', recursive=True): assert '\\' not in path assert ' ' not in path diff --git a/doc/manual/generate-manpage.nix b/doc/manual/generate-manpage.nix index 791bfd2c756..31e74e17d26 100644 --- a/doc/manual/generate-manpage.nix +++ b/doc/manual/generate-manpage.nix @@ -32,7 +32,13 @@ let commandInfo = fromJSON commandDump; - showCommand = { command, details, filename, toplevel }: + showCommand = + { + command, + details, + filename, + toplevel, + }: let result = '' @@ -56,26 +62,27 @@ let ${maybeOptions} ''; - showSynopsis = command: args: + showSynopsis = + command: args: let - showArgument = arg: "*${arg.label}*" + optionalString (! arg ? arity) "..."; + showArgument = arg: "*${arg.label}*" + optionalString (!arg ? arity) "..."; arguments = concatStringsSep " " (map showArgument args); - in '' + in + '' `${command}` [*option*...] ${arguments} ''; - maybeSubcommands = optionalString (details ? commands && details.commands != {}) - '' - where *subcommand* is one of the following: + maybeSubcommands = optionalString (details ? commands && details.commands != { }) '' + where *subcommand* is one of the following: - ${subcommands} - ''; + ${subcommands} + ''; - subcommands = if length categories > 1 - then listCategories - else listSubcommands details.commands; + subcommands = if length categories > 1 then listCategories else listSubcommands details.commands; - categories = sort (x: y: x.id < y.id) (unique (map (cmd: cmd.category) (attrValues details.commands))); + categories = sort (x: y: x.id < y.id) ( + unique (map (cmd: cmd.category) (attrValues details.commands)) + ); listCategories = concatStrings (map showCategory categories); @@ -99,38 +106,39 @@ let ${allStores} ''; - index = replaceStrings - [ "@store-types@" "./local-store.md" "./local-daemon-store.md" ] - [ storesOverview "#local-store" "#local-daemon-store" ] - details.doc; + index = + replaceStrings + [ "@store-types@" "./local-store.md" "./local-daemon-store.md" ] + [ storesOverview "#local-store" "#local-daemon-store" ] + details.doc; storesOverview = let - showEntry = store: - "- [${store.name}](#${store.slug})"; + showEntry = store: "- [${store.name}](#${store.slug})"; in concatStringsSep "\n" (map showEntry storesList) + "\n"; allStores = concatStringsSep "\n" (attrValues storePages); - storePages = listToAttrs - (map (s: { name = s.filename; value = s.page; }) storesList); + storePages = listToAttrs ( + map (s: { + name = s.filename; + value = s.page; + }) storesList + ); storesList = showStoreDocs { storeInfo = commandInfo.stores; inherit inlineHTML; }; - hasInfix = infix: content: + hasInfix = + infix: content: builtins.stringLength content != builtins.stringLength (replaceStrings [ infix ] [ "" ] content); in optionalString (details ? doc) ( # An alternate implementation with builtins.match stack overflowed on some systems. - if hasInfix "@store-types@" details.doc - then help-stores - else details.doc + if hasInfix "@store-types@" details.doc then help-stores else details.doc ); maybeOptions = let - allVisibleOptions = filterAttrs - (_: o: ! o.hiddenCategory) - (details.flags // toplevel.flags); + allVisibleOptions = filterAttrs (_: o: !o.hiddenCategory) (details.flags // toplevel.flags); in optionalString (allVisibleOptions != { }) '' # Options @@ -142,55 +150,73 @@ let > See [`man nix.conf`](@docroot@/command-ref/conf-file.md#command-line-flags) for overriding configuration settings with command line flags. ''; - showOptions = inlineHTML: allOptions: + showOptions = + inlineHTML: allOptions: let showCategory = cat: opts: '' ${optionalString (cat != "") "## ${cat}"} ${concatStringsSep "\n" (attrValues (mapAttrs showOption opts))} ''; - showOption = name: option: + showOption = + name: option: let result = trim '' - ${item} ${option.description} ''; - item = if inlineHTML - then ''[`--${name}`](#opt-${name}) ${shortName} ${labels}'' - else "`--${name}` ${shortName} ${labels}"; - shortName = optionalString - (option ? shortName) - ("/ `-${option.shortName}`"); - labels = optionalString - (option ? labels) - (concatStringsSep " " (map (s: "*${s}*") option.labels)); - in result; - categories = mapAttrs - # Convert each group from a list of key-value pairs back to an attrset - (_: listToAttrs) - (groupBy - (cmd: cmd.value.category) - (attrsToList allOptions)); - in concatStrings (attrValues (mapAttrs showCategory categories)); - in squash result; + item = + if inlineHTML then + ''[`--${name}`](#opt-${name}) ${shortName} ${labels}'' + else + "`--${name}` ${shortName} ${labels}"; + shortName = optionalString (option ? shortName) ("/ `-${option.shortName}`"); + labels = optionalString (option ? labels) (concatStringsSep " " (map (s: "*${s}*") option.labels)); + in + result; + categories = + mapAttrs + # Convert each group from a list of key-value pairs back to an attrset + (_: listToAttrs) + (groupBy (cmd: cmd.value.category) (attrsToList allOptions)); + in + concatStrings (attrValues (mapAttrs showCategory categories)); + in + squash result; appendName = filename: name: (if filename == "nix" then "nix3" else filename) + "-" + name; - processCommand = { command, details, filename, toplevel }: + processCommand = + { + command, + details, + filename, + toplevel, + }: let cmd = { inherit command; name = filename + ".md"; - value = showCommand { inherit command details filename toplevel; }; - }; - subcommand = subCmd: processCommand { - command = command + " " + subCmd; - details = details.commands.${subCmd}; - filename = appendName filename subCmd; - inherit toplevel; + value = showCommand { + inherit + command + details + filename + toplevel + ; + }; }; - in [ cmd ] ++ concatMap subcommand (attrNames details.commands or {}); + subcommand = + subCmd: + processCommand { + command = command + " " + subCmd; + details = details.commands.${subCmd}; + filename = appendName filename subCmd; + inherit toplevel; + }; + in + [ cmd ] ++ concatMap subcommand (attrNames details.commands or { }); manpages = processCommand { command = "nix"; @@ -199,9 +225,11 @@ let toplevel = commandInfo.args; }; - tableOfContents = let - showEntry = page: - " - [${page.command}](command-ref/new-cli/${page.name})"; - in concatStringsSep "\n" (map showEntry manpages) + "\n"; + tableOfContents = + let + showEntry = page: " - [${page.command}](command-ref/new-cli/${page.name})"; + in + concatStringsSep "\n" (map showEntry manpages) + "\n"; -in (listToAttrs manpages) // { "SUMMARY.md" = tableOfContents; } +in +(listToAttrs manpages) // { "SUMMARY.md" = tableOfContents; } diff --git a/doc/manual/generate-settings.nix b/doc/manual/generate-settings.nix index 93a8e093e48..35ae73e5d1f 100644 --- a/doc/manual/generate-settings.nix +++ b/doc/manual/generate-settings.nix @@ -1,67 +1,99 @@ let - inherit (builtins) attrValues concatStringsSep isAttrs isBool mapAttrs; - inherit (import ) concatStrings indent optionalString squash; + inherit (builtins) + attrValues + concatStringsSep + isAttrs + isBool + mapAttrs + ; + inherit (import ) + concatStrings + indent + optionalString + squash + ; in # `inlineHTML` is a hack to accommodate inconsistent output from `lowdown` -{ prefix, inlineHTML ? true }: settingsInfo: +{ + prefix, + inlineHTML ? true, +}: +settingsInfo: let - showSetting = prefix: setting: { description, documentDefault, defaultValue, aliases, value, experimentalFeature }: + showSetting = + prefix: setting: + { + description, + documentDefault, + defaultValue, + aliases, + value, + experimentalFeature, + }: let result = squash '' - - ${item} + - ${item} - ${indent " " body} - ''; - item = if inlineHTML - then ''[`${setting}`](#${prefix}-${setting})'' - else "`${setting}`"; + ${indent " " body} + ''; + item = + if inlineHTML then + ''[`${setting}`](#${prefix}-${setting})'' + else + "`${setting}`"; # separate body to cleanly handle indentation body = '' - ${experimentalFeatureNote} + ${experimentalFeatureNote} - ${description} + ${description} - **Default:** ${showDefault documentDefault defaultValue} + **Default:** ${showDefault documentDefault defaultValue} - ${showAliases aliases} - ''; + ${showAliases aliases} + ''; experimentalFeatureNote = optionalString (experimentalFeature != null) '' - > **Warning** - > - > This setting is part of an - > [experimental feature](@docroot@/development/experimental-features.md). - > - > To change this setting, make sure the - > [`${experimentalFeature}` experimental feature](@docroot@/development/experimental-features.md#xp-feature-${experimentalFeature}) - > is enabled. - > For example, include the following in [`nix.conf`](@docroot@/command-ref/conf-file.md): - > - > ``` - > extra-experimental-features = ${experimentalFeature} - > ${setting} = ... - > ``` - ''; + > **Warning** + > + > This setting is part of an + > [experimental feature](@docroot@/development/experimental-features.md). + > + > To change this setting, make sure the + > [`${experimentalFeature}` experimental feature](@docroot@/development/experimental-features.md#xp-feature-${experimentalFeature}) + > is enabled. + > For example, include the following in [`nix.conf`](@docroot@/command-ref/conf-file.md): + > + > ``` + > extra-experimental-features = ${experimentalFeature} + > ${setting} = ... + > ``` + ''; - showDefault = documentDefault: defaultValue: + showDefault = + documentDefault: defaultValue: if documentDefault then # a StringMap value type is specified as a string, but # this shows the value type. The empty stringmap is `null` in # JSON, but that converts to `{ }` here. - if defaultValue == "" || defaultValue == [] || isAttrs defaultValue - then "*empty*" - else if isBool defaultValue then - if defaultValue then "`true`" else "`false`" - else "`${toString defaultValue}`" - else "*machine-specific*"; + if defaultValue == "" || defaultValue == [ ] || isAttrs defaultValue then + "*empty*" + else if isBool defaultValue then + if defaultValue then "`true`" else "`false`" + else + "`${toString defaultValue}`" + else + "*machine-specific*"; - showAliases = aliases: - optionalString (aliases != []) - "**Deprecated alias:** ${(concatStringsSep ", " (map (s: "`${s}`") aliases))}"; + showAliases = + aliases: + optionalString (aliases != [ ]) + "**Deprecated alias:** ${(concatStringsSep ", " (map (s: "`${s}`") aliases))}"; - in result; + in + result; -in concatStrings (attrValues (mapAttrs (showSetting prefix) settingsInfo)) +in +concatStrings (attrValues (mapAttrs (showSetting prefix) settingsInfo)) diff --git a/doc/manual/generate-store-info.nix b/doc/manual/generate-store-info.nix index cc370412414..e66611affe0 100644 --- a/doc/manual/generate-store-info.nix +++ b/doc/manual/generate-store-info.nix @@ -1,6 +1,20 @@ let - inherit (builtins) attrNames listToAttrs concatStringsSep readFile replaceStrings; - inherit (import ) optionalString filterAttrs trim squash toLower unique indent; + inherit (builtins) + attrNames + listToAttrs + concatStringsSep + readFile + replaceStrings + ; + inherit (import ) + optionalString + filterAttrs + trim + squash + toLower + unique + indent + ; showSettings = import ; in @@ -14,7 +28,14 @@ in let - showStore = { name, slug }: { settings, doc, experimentalFeature }: + showStore = + { name, slug }: + { + settings, + doc, + uri-schemes, + experimentalFeature, + }: let result = squash '' # ${name} @@ -25,7 +46,10 @@ let ## Settings - ${showSettings { prefix = "store-${slug}"; inherit inlineHTML; } settings} + ${showSettings { + prefix = "store-${slug}"; + inherit inlineHTML; + } settings} ''; experimentalFeatureNote = optionalString (experimentalFeature != null) '' @@ -43,15 +67,15 @@ let > extra-experimental-features = ${experimentalFeature} > ``` ''; - in result; - - storesList = map - (name: rec { - inherit name; - slug = replaceStrings [ " " ] [ "-" ] (toLower name); - filename = "${slug}.md"; - page = showStore { inherit name slug; } storeInfo.${name}; - }) - (attrNames storeInfo); - -in storesList + in + result; + + storesList = map (name: rec { + inherit name; + slug = replaceStrings [ " " ] [ "-" ] (toLower name); + filename = "${slug}.md"; + page = showStore { inherit name slug; } storeInfo.${name}; + }) (attrNames storeInfo); + +in +storesList diff --git a/doc/manual/generate-store-types.nix b/doc/manual/generate-store-types.nix index 46179abc5bf..a03d3d6216e 100644 --- a/doc/manual/generate-store-types.nix +++ b/doc/manual/generate-store-types.nix @@ -1,5 +1,11 @@ let - inherit (builtins) attrNames listToAttrs concatStringsSep readFile replaceStrings; + inherit (builtins) + attrNames + listToAttrs + concatStringsSep + readFile + replaceStrings + ; showSettings = import ; showStoreDocs = import ; in @@ -14,26 +20,28 @@ let index = let - showEntry = store: - "- [${store.name}](./${store.filename})"; + showEntry = store: "- [${store.name}](./${store.filename})"; in concatStringsSep "\n" (map showEntry storesList); - "index.md" = replaceStrings - [ "@store-types@" ] [ index ] - (readFile ./source/store/types/index.md.in); + "index.md" = + replaceStrings [ "@store-types@" ] [ index ] + (readFile ./source/store/types/index.md.in); tableOfContents = let - showEntry = store: - " - [${store.name}](store/types/${store.filename})"; + showEntry = store: " - [${store.name}](store/types/${store.filename})"; in concatStringsSep "\n" (map showEntry storesList) + "\n"; "SUMMARY.md" = tableOfContents; - storePages = listToAttrs - (map (s: { name = s.filename; value = s.page; }) storesList); + storePages = listToAttrs ( + map (s: { + name = s.filename; + value = s.page; + }) storesList + ); in storePages // { inherit "index.md" "SUMMARY.md"; } diff --git a/doc/manual/generate-xp-features-shortlist.nix b/doc/manual/generate-xp-features-shortlist.nix index eb735ba5f7a..1520fc2f815 100644 --- a/doc/manual/generate-xp-features-shortlist.nix +++ b/doc/manual/generate-xp-features-shortlist.nix @@ -2,8 +2,8 @@ with builtins; with import ; let - showExperimentalFeature = name: doc: - '' - - [`${name}`](@docroot@/development/experimental-features.md#xp-feature-${name}) - ''; -in xps: indent " " (concatStrings (attrValues (mapAttrs showExperimentalFeature xps))) + showExperimentalFeature = name: doc: '' + - [`${name}`](@docroot@/development/experimental-features.md#xp-feature-${name}) + ''; +in +xps: indent " " (concatStrings (attrValues (mapAttrs showExperimentalFeature xps))) diff --git a/doc/manual/generate-xp-features.nix b/doc/manual/generate-xp-features.nix index 0eec0e1da23..468d253bafd 100644 --- a/doc/manual/generate-xp-features.nix +++ b/doc/manual/generate-xp-features.nix @@ -2,7 +2,8 @@ with builtins; with import ; let - showExperimentalFeature = name: doc: + showExperimentalFeature = + name: doc: squash '' ## [`${name}`]{#xp-feature-${name}} diff --git a/doc/manual/meson.build b/doc/manual/meson.build index 3630e2dc828..6fe2374a764 100644 --- a/doc/manual/meson.build +++ b/doc/manual/meson.build @@ -67,7 +67,7 @@ subdir('source/release-notes') subdir('source') # Hacky way to figure out if `nix` is an `ExternalProgram` or -# `Exectuable`. Only the latter can occur in custom target input lists. +# `Executable`. Only the latter can occur in custom target input lists. if nix.full_path().startswith(meson.build_root()) nix_input = nix else @@ -83,6 +83,7 @@ manual = custom_target( ''' @0@ @INPUT0@ @CURRENT_SOURCE_DIR@ > @DEPFILE@ @0@ @INPUT1@ summary @2@ < @CURRENT_SOURCE_DIR@/source/SUMMARY.md.in > @2@/source/SUMMARY.md + sed -e 's|@version@|@3@|g' < @INPUT2@ > @2@/book.toml rsync -r --include='*.md' @CURRENT_SOURCE_DIR@/ @2@/ (cd @2@; RUST_LOG=warn @1@ build -d @2@ 3>&2 2>&1 1>&3) | { grep -Fv "because fragment resolution isn't implemented" || :; } 3>&2 2>&1 1>&3 rm -rf @2@/manual @@ -92,12 +93,13 @@ manual = custom_target( python.full_path(), mdbook.full_path(), meson.current_build_dir(), + meson.project_version(), ), ], input : [ generate_manual_deps, 'substitute.py', - 'book.toml', + 'book.toml.in', 'anchors.jq', 'custom.css', nix3_cli_files, @@ -199,6 +201,7 @@ nix3_manpages = [ 'nix3-build', 'nix3-bundle', 'nix3-config', + 'nix3-config-check', 'nix3-config-show', 'nix3-copy', 'nix3-daemon', @@ -206,8 +209,8 @@ nix3_manpages = [ 'nix3-derivation', 'nix3-derivation-show', 'nix3-develop', - #'nix3-doctor', 'nix3-edit', + 'nix3-env-shell', 'nix3-eval', 'nix3-flake-archive', 'nix3-flake-check', @@ -224,6 +227,7 @@ nix3_manpages = [ 'nix3-fmt', 'nix3-hash-file', 'nix3-hash', + 'nix3-hash-convert', 'nix3-hash-path', 'nix3-hash-to-base16', 'nix3-hash-to-base32', @@ -238,14 +242,15 @@ nix3_manpages = [ 'nix3-nar-cat', 'nix3-nar-dump-path', 'nix3-nar-ls', + 'nix3-nar-pack', 'nix3-nar', 'nix3-path-info', 'nix3-print-dev-env', + 'nix3-profile', + 'nix3-profile-add', 'nix3-profile-diff-closures', 'nix3-profile-history', - 'nix3-profile-install', 'nix3-profile-list', - 'nix3-profile', 'nix3-profile-remove', 'nix3-profile-rollback', 'nix3-profile-upgrade', @@ -260,7 +265,7 @@ nix3_manpages = [ 'nix3-repl', 'nix3-run', 'nix3-search', - #'nix3-shell', + 'nix3-store-add', 'nix3-store-add-file', 'nix3-store-add-path', 'nix3-store-cat', @@ -270,12 +275,12 @@ nix3_manpages = [ 'nix3-store-diff-closures', 'nix3-store-dump-path', 'nix3-store-gc', + 'nix3-store-info', 'nix3-store-ls', 'nix3-store-make-content-addressed', 'nix3-store', 'nix3-store-optimise', 'nix3-store-path-from-hash-part', - 'nix3-store-ping', 'nix3-store-prefetch-file', 'nix3-store-repair', 'nix3-store-sign', diff --git a/doc/manual/package.nix b/doc/manual/package.nix index 2e6fcede3f7..af6d46a2a00 100644 --- a/doc/manual/package.nix +++ b/doc/manual/package.nix @@ -1,19 +1,22 @@ -{ lib -, mkMesonDerivation +{ + lib, + mkMesonDerivation, -, meson -, ninja -, lowdown -, mdbook -, mdbook-linkcheck -, jq -, python3 -, rsync -, nix-cli + meson, + ninja, + lowdown-unsandboxed, + mdbook, + mdbook-linkcheck, + jq, + python3, + rsync, + nix-cli, + changelog-d, + officialRelease, -# Configuration Options + # Configuration Options -, version + version, }: let @@ -25,40 +28,51 @@ mkMesonDerivation (finalAttrs: { inherit version; workDir = ./.; - fileset = fileset.difference - (fileset.unions [ - ../../.version - # Too many different types of files to filter for now - ../../doc/manual - ./. - ]) - # Do a blacklist instead - ../../doc/manual/package.nix; + fileset = + fileset.difference + (fileset.unions [ + ../../.version + # Too many different types of files to filter for now + ../../doc/manual + ./. + ]) + # Do a blacklist instead + ../../doc/manual/package.nix; # TODO the man pages should probably be separate - outputs = [ "out" "man" ]; + outputs = [ + "out" + "man" + ]; # Hack for sake of the dev shell - passthru.externalNativeBuildInputs = [ - meson - ninja - (lib.getBin lowdown) - mdbook - mdbook-linkcheck - jq - python3 - rsync - ]; + passthru.externalNativeBuildInputs = + [ + meson + ninja + (lib.getBin lowdown-unsandboxed) + mdbook + mdbook-linkcheck + jq + python3 + rsync + changelog-d + ] + ++ lib.optionals (!officialRelease) [ + # When not an official release, we likely have changelog entries that have + # yet to be rendered. + # When released, these are rendered into a committed file to save a dependency. + changelog-d + ]; nativeBuildInputs = finalAttrs.passthru.externalNativeBuildInputs ++ [ nix-cli ]; - preConfigure = - '' - chmod u+w ./.version - echo ${finalAttrs.version} > ./.version - ''; + preConfigure = '' + chmod u+w ./.version + echo ${finalAttrs.version} > ./.version + ''; postInstall = '' mkdir -p ''$out/nix-support diff --git a/doc/manual/redirects.js b/doc/manual/redirects.js index dea141391df..9612438481f 100644 --- a/doc/manual/redirects.js +++ b/doc/manual/redirects.js @@ -346,6 +346,9 @@ const redirects = { "scoping-rules": "scoping.html", "string-literal": "string-literals.html", }, + "language/derivations.md": { + "builder-execution": "store/drv/building.md#builder-execution", + }, "installation/installing-binary.html": { "linux": "uninstall.html#linux", "macos": "uninstall.html#macos", @@ -371,7 +374,9 @@ const redirects = { }, "glossary.html": { "gloss-local-store": "store/types/local-store.html", + "package-attribute-set": "#package", "gloss-chroot-store": "store/types/local-store.html", + "gloss-content-addressed-derivation": "#gloss-content-addressing-derivation", }, }; diff --git a/doc/manual/rl-next/build-cores-auto-detect.md b/doc/manual/rl-next/build-cores-auto-detect.md new file mode 100644 index 00000000000..67ab6995b4c --- /dev/null +++ b/doc/manual/rl-next/build-cores-auto-detect.md @@ -0,0 +1,6 @@ +--- +synopsis: "`build-cores = 0` now auto-detects CPU cores" +prs: [13402] +--- + +When `build-cores` is set to `0`, nix now automatically detects the number of available CPU cores and passes this value via `NIX_BUILD_CORES`, instead of passing `0` directly. This matches the behavior when `build-cores` is unset. This prevents the builder from having to detect the number of cores. diff --git a/doc/manual/rl-next/nix-copy-flags.md b/doc/manual/rl-next/nix-copy-flags.md deleted file mode 100644 index f5b2b9716a4..00000000000 --- a/doc/manual/rl-next/nix-copy-flags.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -synopsis: "`nix copy` supports `--profile` and `--out-link`" -prs: [11657] ---- - -The `nix copy` command now has flags `--profile` and `--out-link`, similar to `nix build`. `--profile` makes a profile point to the -top-level store path, while `--out-link` create symlinks to the top-level store paths. - -For example, when updating the local NixOS system profile from a NixOS system closure on a remote machine, instead of -``` -# nix copy --from ssh://server $path -# nix build --profile /nix/var/nix/profiles/system $path -``` -you can now do -``` -# nix copy --from ssh://server --profile /nix/var/nix/profiles/system $path -``` -The advantage is that this avoids a time window where *path* is not a garbage collector root, and so could be deleted by a concurrent `nix store gc` process. diff --git a/doc/manual/source/SUMMARY.md.in b/doc/manual/source/SUMMARY.md.in index 244bf582d09..bfb921567c3 100644 --- a/doc/manual/source/SUMMARY.md.in +++ b/doc/manual/source/SUMMARY.md.in @@ -22,12 +22,18 @@ - [Store Object](store/store-object.md) - [Content-Addressing Store Objects](store/store-object/content-address.md) - [Store Path](store/store-path.md) + - [Store Derivation and Deriving Path](store/derivation/index.md) + - [Derivation Outputs and Types of Derivations](store/derivation/outputs/index.md) + - [Content-addressing derivation outputs](store/derivation/outputs/content-address.md) + - [Input-addressing derivation outputs](store/derivation/outputs/input-address.md) + - [Building](store/building.md) - [Store Types](store/types/index.md) {{#include ./store/types/SUMMARY.md}} - [Nix Language](language/index.md) - [Data Types](language/types.md) - [String context](language/string-context.md) - [Syntax and semantics](language/syntax.md) + - [Evaluation](language/evaluation.md) - [Variables](language/variables.md) - [String literals](language/string-literals.md) - [Identifiers](language/identifiers.md) @@ -51,6 +57,7 @@ - [Tuning Cores and Jobs](advanced-topics/cores-vs-jobs.md) - [Verifying Build Reproducibility](advanced-topics/diff-hook.md) - [Using the `post-build-hook`](advanced-topics/post-build-hook.md) + - [Evaluation profiler](advanced-topics/eval-profiler.md) - [Command Reference](command-ref/index.md) - [Common Options](command-ref/opt-common.md) - [Common Environment Variables](command-ref/env-common.md) @@ -130,6 +137,11 @@ - [Contributing](development/contributing.md) - [Releases](release-notes/index.md) {{#include ./SUMMARY-rl-next.md}} + - [Release 2.30 (2025-07-07)](release-notes/rl-2.30.md) + - [Release 2.29 (2025-05-14)](release-notes/rl-2.29.md) + - [Release 2.28 (2025-04-02)](release-notes/rl-2.28.md) + - [Release 2.27 (2025-03-03)](release-notes/rl-2.27.md) + - [Release 2.26 (2025-01-22)](release-notes/rl-2.26.md) - [Release 2.25 (2024-11-07)](release-notes/rl-2.25.md) - [Release 2.24 (2024-07-31)](release-notes/rl-2.24.md) - [Release 2.23 (2024-06-03)](release-notes/rl-2.23.md) diff --git a/doc/manual/source/advanced-topics/distributed-builds.md b/doc/manual/source/advanced-topics/distributed-builds.md index 66e37188840..08a980643e8 100644 --- a/doc/manual/source/advanced-topics/distributed-builds.md +++ b/doc/manual/source/advanced-topics/distributed-builds.md @@ -20,14 +20,14 @@ For a local machine to forward a build to a remote machine, the remote machine m ## Testing -To test connecting to a remote Nix instance (in this case `mac`), run: +To test connecting to a remote [Nix instance] (in this case `mac`), run: ```console nix store info --store ssh://username@mac ``` To specify an SSH identity file as part of the remote store URI add a -query paramater, e.g. +query parameter, e.g. ```console nix store info --store ssh://username@mac?ssh-key=/home/alice/my-key @@ -106,3 +106,5 @@ file included in `builders` via the syntax `@/path/to/file`. For example, causes the list of machines in `/etc/nix/machines` to be included. (This is the default.) + +[Nix instance]: @docroot@/glossary.md#gloss-nix-instance \ No newline at end of file diff --git a/doc/manual/source/advanced-topics/eval-profiler.md b/doc/manual/source/advanced-topics/eval-profiler.md new file mode 100644 index 00000000000..ed3848bb2db --- /dev/null +++ b/doc/manual/source/advanced-topics/eval-profiler.md @@ -0,0 +1,33 @@ +# Using the `eval-profiler` + +Nix evaluator supports [evaluation](@docroot@/language/evaluation.md) +[profiling]() +compatible with `flamegraph.pl`. The profiler samples the nix +function call stack at regular intervals. It can be enabled with the +[`eval-profiler`](@docroot@/command-ref/conf-file.md#conf-eval-profiler) +setting: + +```console +$ nix-instantiate "" -A hello --eval-profiler flamegraph +``` + +Stack sampling frequency and the output file path can be configured with +[`eval-profile-file`](@docroot@/command-ref/conf-file.md#conf-eval-profile-file) +and [`eval-profiler-frequency`](@docroot@/command-ref/conf-file.md#conf-eval-profiler-frequency). +By default the collected profile is saved to `nix.profile` file in the current working directory. + +The collected profile can be directly consumed by `flamegraph.pl`: + +```console +$ flamegraph.pl nix.profile > flamegraph.svg +``` + +The line information in the profile contains the location of the [call +site](https://en.wikipedia.org/wiki/Call_site) position and the name of the +function being called (when available). For example: + +``` +/nix/store/x9wnkly3k1gkq580m90jjn32q9f05q2v-source/pkgs/top-level/default.nix:167:5:primop import +``` + +Here `import` primop is called at `/nix/store/x9wnkly3k1gkq580m90jjn32q9f05q2v-source/pkgs/top-level/default.nix:167:5`. diff --git a/doc/manual/source/architecture/architecture.md b/doc/manual/source/architecture/architecture.md index 867a9c992d3..0c8677a6a56 100644 --- a/doc/manual/source/architecture/architecture.md +++ b/doc/manual/source/architecture/architecture.md @@ -22,9 +22,9 @@ The following [concept map] shows its main components (rectangles), the objects | | +----------|-------------------|--------------------------------+ | Nix | V | -| | +-------------------------+ | -| | | commmand line interface |------. | -| | +-------------------------+ | | +| | +------------------------+ | +| | | command line interface |------. | +| | +------------------------+ | | | | | | | | evaluated by calls manages | | | | | | @@ -69,7 +69,7 @@ It can also execute build plans to produce new data, which are made available to A build plan itself is a series of *build tasks*, together with their build inputs. > **Important** -> A build task in Nix is called [derivation](@docroot@/glossary.md#gloss-derivation). +> A build task in Nix is called [store derivation](@docroot@/glossary.md#gloss-store-derivation). Each build task has a special build input executed as *build instructions* in order to perform the build. The result of a build task can be input to another build task. diff --git a/doc/manual/source/command-ref/nix-channel.md b/doc/manual/source/command-ref/nix-channel.md index 8b58392b7b5..ed9cbb41fbf 100644 --- a/doc/manual/source/command-ref/nix-channel.md +++ b/doc/manual/source/command-ref/nix-channel.md @@ -53,6 +53,11 @@ This command has the following operations: Download the Nix expressions of subscribed channels and create a new generation. Update all channels if none is specified, and only those included in *names* otherwise. + > **Note** + > + > Downloaded channel contents are cached. + > Use `--tarball-ttl` or the [`tarball-ttl` configuration option](@docroot@/command-ref/conf-file.md#conf-tarball-ttl) to change the validity period of cached downloads. + - `--list-generations` Prints a list of all the current existing generations for the diff --git a/doc/manual/source/command-ref/nix-collect-garbage.md b/doc/manual/source/command-ref/nix-collect-garbage.md index bd05f28164e..763179b8ee1 100644 --- a/doc/manual/source/command-ref/nix-collect-garbage.md +++ b/doc/manual/source/command-ref/nix-collect-garbage.md @@ -62,6 +62,15 @@ These options are for deleting old [profiles] prior to deleting unreachable [sto This is the equivalent of invoking [`nix-env --delete-generations `](@docroot@/command-ref/nix-env/delete-generations.md#generations-time) on each found profile. See the documentation of that command for additional information about the *period* argument. + - [`--max-freed`](#opt-max-freed) *bytes* + + + + Keep deleting paths until at least *bytes* bytes have been deleted, + then stop. The argument *bytes* can be followed by the + multiplicative suffix `K`, `M`, `G` or `T`, denoting KiB, MiB, GiB + or TiB units. + {{#include ./opt-common.md}} {{#include ./env-common.md}} diff --git a/doc/manual/source/command-ref/nix-copy-closure.md b/doc/manual/source/command-ref/nix-copy-closure.md index 8cfd6ebad56..b7e31d93bfc 100644 --- a/doc/manual/source/command-ref/nix-copy-closure.md +++ b/doc/manual/source/command-ref/nix-copy-closure.md @@ -84,7 +84,7 @@ When using public key authentication, you can avoid typing the passphrase with ` > Copy GNU Hello from a remote machine using a known store path, and run it: > > ```shell-session -> $ storePath="$(nix-instantiate --eval '' -I nixpkgs=channel:nixpkgs-unstable -A hello.outPath | tr -d '"')" +> $ storePath="$(nix-instantiate --eval --raw '' -I nixpkgs=channel:nixpkgs-unstable -A hello.outPath)" > $ nix-copy-closure --from alice@itchy.example.org "$storePath" > $ "$storePath"/bin/hello > Hello, world! diff --git a/doc/manual/source/command-ref/nix-env/delete-generations.md b/doc/manual/source/command-ref/nix-env/delete-generations.md index b1ff0bb6941..b769790fd45 100644 --- a/doc/manual/source/command-ref/nix-env/delete-generations.md +++ b/doc/manual/source/command-ref/nix-env/delete-generations.md @@ -27,7 +27,7 @@ This operation deletes the specified generations of the current profile. > > Older *and newer* generations will be deleted by this operation. > - > One might expect this to just delete older generations than the curent one, but that is only true if the current generation is also the latest. + > One might expect this to just delete older generations than the current one, but that is only true if the current generation is also the latest. > Because one can roll back to a previous generation, it is possible to have generations newer than the current one. > They will also be deleted. diff --git a/doc/manual/source/command-ref/nix-env/install.md b/doc/manual/source/command-ref/nix-env/install.md index 748dd1e7a39..527fd8f90d8 100644 --- a/doc/manual/source/command-ref/nix-env/install.md +++ b/doc/manual/source/command-ref/nix-env/install.md @@ -11,6 +11,7 @@ [`--from-profile` *path*] [`--preserve-installed` | `-P`] [`--remove-all` | `-r`] + [`--priority` *priority*] # Description @@ -21,11 +22,11 @@ It is based on the current generation of the active [profile](@docroot@/command- The arguments *args* map to store paths in a number of possible ways: -- By default, *args* is a set of [derivation] names denoting derivations in the [default Nix expression]. +- By default, *args* is a set of names denoting derivations in the [default Nix expression]. These are [realised], and the resulting output paths are installed. Currently installed derivations with a name equal to the name of a derivation being added are removed unless the option `--preserve-installed` is specified. - [derivation]: @docroot@/glossary.md#gloss-derivation + [derivation expression]: @docroot@/glossary.md#gloss-derivation-expression [default Nix expression]: @docroot@/command-ref/files/default-nix-expression.md [realised]: @docroot@/glossary.md#gloss-realise @@ -61,11 +62,15 @@ The arguments *args* map to store paths in a number of possible ways: The derivations returned by those function calls are installed. This allows derivations to be specified in an unambiguous way, which is necessary if there are multiple derivations with the same name. -- If *args* are [store derivations](@docroot@/glossary.md#gloss-store-derivation), then these are [realised], and the resulting output paths are installed. +- If `--priority` *priority* is given, the priority of the derivations being installed is set to *priority*. + This can be used to override the priority of the derivations being installed. + This is useful if *args* are [store paths], which don't have any priority information. -- If *args* are [store paths] that are not store derivations, then these are [realised] and installed. +- If *args* are [store paths] that point to [store derivations][store derivation], then those store derivations are [realised], and the resulting output paths are installed. -- By default all [outputs](@docroot@/language/derivations.md#attr-outputs) are installed for each [derivation]. +- If *args* are [store paths] that do not point to store derivations, then these are [realised] and installed. + +- By default all [outputs](@docroot@/language/derivations.md#attr-outputs) are installed for each [store derivation]. This can be overridden by adding a `meta.outputsToInstall` attribute on the derivation listing a subset of the output names. Example: @@ -117,6 +122,8 @@ The arguments *args* map to store paths in a number of possible ways: manifest.nix ``` +[store derivation]: @docroot@/glossary.md#gloss-store-derivation + # Options - `--prebuilt-only` / `-b` @@ -235,4 +242,3 @@ channel: ```console $ nix-env --file https://github.com/NixOS/nixpkgs/archive/nixos-14.12.tar.gz --install --attr firefox ``` - diff --git a/doc/manual/source/command-ref/nix-env/query.md b/doc/manual/source/command-ref/nix-env/query.md index c67794ed58e..bde9b38202c 100644 --- a/doc/manual/source/command-ref/nix-env/query.md +++ b/doc/manual/source/command-ref/nix-env/query.md @@ -125,7 +125,10 @@ derivation is shown unless `--no-name` is specified. - `--drv-path` - Print the path of the [store derivation](@docroot@/glossary.md#gloss-store-derivation). + Print the [store path] to the [store derivation]. + + [store path]: @docroot@/glossary.md#gloss-store-path + [store derivation]: @docroot@/glossary.md#gloss-derivation - `--out-path` diff --git a/doc/manual/source/command-ref/nix-hash.md b/doc/manual/source/command-ref/nix-hash.md index f249c2b84d6..0860f312d94 100644 --- a/doc/manual/source/command-ref/nix-hash.md +++ b/doc/manual/source/command-ref/nix-hash.md @@ -67,7 +67,7 @@ md5sum`. - `--type` *hashAlgo* Use the specified cryptographic hash algorithm, which can be one of - `md5`, `sha1`, `sha256`, and `sha512`. + `blake3`, `md5`, `sha1`, `sha256`, and `sha512`. - `--to-base16` diff --git a/doc/manual/source/command-ref/nix-instantiate.md b/doc/manual/source/command-ref/nix-instantiate.md index 6f6fcdc1f66..38454515d57 100644 --- a/doc/manual/source/command-ref/nix-instantiate.md +++ b/doc/manual/source/command-ref/nix-instantiate.md @@ -5,7 +5,7 @@ # Synopsis `nix-instantiate` - [`--parse` | `--eval` [`--strict`] [`--json`] [`--xml`] ] + [`--parse` | `--eval` [`--strict`] [`--raw` | `--json` | `--xml`] ] [`--read-write-mode`] [`--arg` *name* *value*] [{`--attr`| `-A`} *attrPath*] @@ -42,8 +42,8 @@ standard input. - `--eval` Just parse and evaluate the input files, and print the resulting - values on standard output. No instantiation of store derivations - takes place. + values on standard output. + Store derivations are not serialized and written to the store, but instead just hashed and discarded. > **Warning** > @@ -102,6 +102,11 @@ standard input. > This option can cause non-termination, because lazy data > structures can be infinitely large. +- `--raw` + + When used with `--eval`, the evaluation result must be a string, + which is printed verbatim, without quoting, escaping or trailing newline. + - `--json` When used with `--eval`, print the resulting value as an JSON diff --git a/doc/manual/source/command-ref/nix-prefetch-url.md b/doc/manual/source/command-ref/nix-prefetch-url.md index ffab94b8afa..19322ec8e04 100644 --- a/doc/manual/source/command-ref/nix-prefetch-url.md +++ b/doc/manual/source/command-ref/nix-prefetch-url.md @@ -42,7 +42,7 @@ the path of the downloaded file in the Nix store is also printed. - `--type` *hashAlgo* Use the specified cryptographic hash algorithm, - which can be one of `md5`, `sha1`, `sha256`, and `sha512`. + which can be one of `blake3`, `md5`, `sha1`, `sha256`, and `sha512`. The default is `sha256`. - `--print-path` diff --git a/doc/manual/source/command-ref/nix-shell.md b/doc/manual/source/command-ref/nix-shell.md index e95db9bea5d..f2e2e35936e 100644 --- a/doc/manual/source/command-ref/nix-shell.md +++ b/doc/manual/source/command-ref/nix-shell.md @@ -242,16 +242,21 @@ print(t) ``` Similarly, the following is a Perl script that specifies that it -requires Perl and the `HTML::TokeParser::Simple` and `LWP` packages: +requires Perl and the `HTML::TokeParser::Simple`, `LWP` and +`LWP::Protocol::Https` packages: ```perl #! /usr/bin/env nix-shell -#! nix-shell -i perl --packages perl perlPackages.HTMLTokeParserSimple perlPackages.LWP +#! nix-shell -i perl +#! nix-shell --packages perl +#! nix-shell --packages perlPackages.HTMLTokeParserSimple +#! nix-shell --packages perlPackages.LWP +#! nix-shell --packages perlPackages.LWPProtocolHttps use HTML::TokeParser::Simple; # Fetch nixos.org and print all hrefs. -my $p = HTML::TokeParser::Simple->new(url => 'http://nixos.org/'); +my $p = HTML::TokeParser::Simple->new(url => 'https://nixos.org/'); while (my $token = $p->get_tag("a")) { my $href = $token->get_attr("href"); @@ -316,7 +321,7 @@ contains: ```nix with import {}; -runCommand "dummy" { buildInputs = [ python pythonPackages.prettytable ]; } "" +runCommand "dummy" { buildInputs = [ python3 python3Packages.prettytable ]; } "" ``` The script's file name is passed as the first argument to the interpreter specified by the `-i` flag. diff --git a/doc/manual/source/command-ref/nix-store/add-fixed.md b/doc/manual/source/command-ref/nix-store/add-fixed.md index bebf15026a6..2ea90a13592 100644 --- a/doc/manual/source/command-ref/nix-store/add-fixed.md +++ b/doc/manual/source/command-ref/nix-store/add-fixed.md @@ -21,6 +21,9 @@ This operation has the following options: Use recursive instead of flat hashing mode, used when adding directories to the store. + *paths* that refer to symlinks are not dereferenced, but added to the store + as symlinks with the same target. + {{#include ./opt-common.md}} {{#include ../opt-common.md}} diff --git a/doc/manual/source/command-ref/nix-store/add.md b/doc/manual/source/command-ref/nix-store/add.md index 87d504cd333..ab474072311 100644 --- a/doc/manual/source/command-ref/nix-store/add.md +++ b/doc/manual/source/command-ref/nix-store/add.md @@ -11,6 +11,9 @@ The operation `--add` adds the specified paths to the Nix store. It prints the resulting paths in the Nix store on standard output. +*paths* that refer to symlinks are not dereferenced, but added to the store +as symlinks with the same target. + {{#include ./opt-common.md}} {{#include ../opt-common.md}} diff --git a/doc/manual/source/command-ref/nix-store/query.md b/doc/manual/source/command-ref/nix-store/query.md index 601f46af6b2..b5ba63adae2 100644 --- a/doc/manual/source/command-ref/nix-store/query.md +++ b/doc/manual/source/command-ref/nix-store/query.md @@ -45,10 +45,19 @@ symlink. [output paths]: @docroot@/glossary.md#gloss-output-path +- `--references` + + Prints the set of [references] of the store paths + *paths*, that is, their immediate dependencies. (For *all* + dependencies, use `--requisites`.) + + [references]: @docroot@/glossary.md#gloss-reference + - `--requisites` / `-R` - Prints out the [closure] of the store path *paths*. + Prints out the set of [*requisites*][requisite] (better known as the [closure]) of the store path *paths*. + [requisite]: @docroot@/glossary.md#gloss-requisite [closure]: @docroot@/glossary.md#gloss-closure This query has one option: @@ -65,29 +74,25 @@ symlink. dependencies) is obtained by distributing the closure of a store derivation and specifying the option `--include-outputs`. -- `--references` - - Prints the set of [references] of the store paths - *paths*, that is, their immediate dependencies. (For *all* - dependencies, use `--requisites`.) - - [references]: @docroot@/glossary.md#gloss-reference - - `--referrers` - Prints the set of *referrers* of the store paths *paths*, that is, + Prints the set of [*referrers*][referrer] of the store paths *paths*, that is, the store paths currently existing in the Nix store that refer to one of *paths*. Note that contrary to the references, the set of referrers is not constant; it can change as store paths are added or removed. + [referrer]: @docroot@/glossary.md#gloss-referrer + - `--referrers-closure` Prints the closure of the set of store paths *paths* under the - referrers relation; that is, all store paths that directly or + [referrers relation][referrer]; that is, all store paths that directly or indirectly refer to one of *paths*. These are all the path currently in the Nix store that are dependent on *paths*. + [referrer]: @docroot@/glossary.md#gloss-referrer + - `--deriver` / `-d` Prints the [deriver] that was used to build the store paths *paths*. If diff --git a/doc/manual/source/command-ref/nix-store/realise.md b/doc/manual/source/command-ref/nix-store/realise.md index a899758dfab..240685ce5c7 100644 --- a/doc/manual/source/command-ref/nix-store/realise.md +++ b/doc/manual/source/command-ref/nix-store/realise.md @@ -15,7 +15,7 @@ Each of *paths* is processed as follows: 1. If it is not [valid], substitute the store derivation file itself. 2. Realise its [output paths]: - Try to fetch from [substituters] the [store objects] associated with the output paths in the store derivation's [closure]. - - With [content-addressed derivations] (experimental): + - With [content-addressing derivations] (experimental): Determine the output paths to realise by querying content-addressed realisation entries in the [Nix database]. - For any store paths that cannot be substituted, produce the required store objects: 1. Realise all outputs of the derivation's dependencies @@ -32,7 +32,7 @@ If no substitutes are available and no store derivation is given, realisation fa [store objects]: @docroot@/store/store-object.md [closure]: @docroot@/glossary.md#gloss-closure [substituters]: @docroot@/command-ref/conf-file.md#conf-substituters -[content-addressed derivations]: @docroot@/development/experimental-features.md#xp-feature-ca-derivations +[content-addressing derivations]: @docroot@/development/experimental-features.md#xp-feature-ca-derivations [Nix database]: @docroot@/glossary.md#gloss-nix-database The resulting paths are printed on standard output. diff --git a/doc/manual/source/development/building.md b/doc/manual/source/development/building.md index 409294682e9..16746383704 100644 --- a/doc/manual/source/development/building.md +++ b/doc/manual/source/development/building.md @@ -28,7 +28,7 @@ $ nix-shell --attr devShells.x86_64-linux.native-clangStdenvPackages > **Note** > -> You can use `native-ccacheStdenvPackages` to drastically improve rebuild time. +> You can use `native-ccacheStdenv` to drastically improve rebuild time. > By default, [ccache](https://ccache.dev) keeps artifacts in `~/.cache/ccache/`. To build Nix itself in this shell: @@ -79,7 +79,7 @@ This shell also adds `./outputs/bin/nix` to your `$PATH` so you can run `nix` im To get a shell with one of the other [supported compilation environments](#compilation-environments): ```console -$ nix develop .#native-clangStdenvPackages +$ nix develop .#native-clangStdenv ``` > **Note** @@ -167,24 +167,18 @@ It is useful to perform multiple cross and native builds on the same source tree for example to ensure that better support for one platform doesn't break the build for another. Meson thankfully makes this very easy by confining all build products to the build directory --- one simple shares the source directory between multiple build directories, each of which contains the build for Nix to a different platform. -Nixpkgs's `configurePhase` always chooses `build` in the current directory as the name and location of the build. -This makes having multiple build directories slightly more inconvenient. -The good news is that Meson/Ninja seem to cope well with relocating the build directory after it is created. +Here's how to do that: -Here's how to do that - -1. Configure as usual +1. Instruct Nixpkgs's infra where we want Meson to put its build directory ```bash - configurePhase + mesonBuildDir=build-my-variant-name ``` -2. Rename the build directory +1. Configure as usual ```bash - cd .. # since `configurePhase` cd'd inside - mv build build-linux # or whatever name we want - cd build-linux + configurePhase ``` 3. Build as usual @@ -193,10 +187,6 @@ Here's how to do that buildPhase ``` -> **N.B.** -> [`nixpkgs#335818`](https://github.com/NixOS/nixpkgs/issues/335818) tracks giving `mesonConfigurePhase` proper support for custom build directories. -> When it is fixed, we can simplify these instructions and then remove this notice. - ## System type Nix uses a string with the following format to identify the *system type* or *platform* it runs on: @@ -205,19 +195,25 @@ Nix uses a string with the following format to identify the *system type* or *pl -[-] ``` -It is set when Nix is compiled for the given system, and based on the output of [`config.guess`](https://github.com/nixos/nix/blob/master/config/config.guess) ([upstream](https://git.savannah.gnu.org/cgit/config.git/tree/config.guess)): +It is set when Nix is compiled for the given system, and based on the output of Meson's [`host_machine` information](https://mesonbuild.com/Reference-manual_builtin_host_machine.html)> ``` --[][-] ``` -When Nix is built such that `./configure` is passed any of the `--host`, `--build`, `--target` options, the value is based on the output of [`config.sub`](https://github.com/nixos/nix/blob/master/config/config.sub) ([upstream](https://git.savannah.gnu.org/cgit/config.git/tree/config.sub)): +When cross-compiling Nix with Meson for local development, you need to specify a [cross-file](https://mesonbuild.com/Cross-compilation.html) using the `--cross-file` option. Cross-files define the target architecture and toolchain. When cross-compiling Nix with Nix, Nixpkgs takes care of this for you. + +In the nix flake we also have some cross-compilation targets available: ``` --[-]- +nix build .#nix-everything-riscv64-unknown-linux-gnu +nix build .#nix-everything-armv7l-unknown-linux-gnueabihf +nix build .#nix-everything-armv7l-unknown-linux-gnueabihf +nix build .#nix-everything-x86_64-unknown-freebsd +nix build .#nix-everything-x86_64-w64-mingw32 ``` -For historic reasons and backward-compatibility, some CPU and OS identifiers are translated from the GNU Autotools naming convention in [`configure.ac`](https://github.com/nixos/nix/blob/master/configure.ac) as follows: +For historic reasons and backward-compatibility, some CPU and OS identifiers are translated as follows: | `config.guess` | Nix | |----------------------------|---------------------| @@ -240,18 +236,18 @@ Nix can be compiled using multiple environments: To build with one of those environments, you can use ```console -$ nix build .#nix-ccacheStdenv +$ nix build .#nix-cli-ccacheStdenv ``` for flake-enabled Nix, or ```console -$ nix-build --attr nix-ccacheStdenv +$ nix-build --attr nix-cli-ccacheStdenv ``` for classic Nix. -You can use any of the other supported environments in place of `nix-ccacheStdenv`. +You can use any of the other supported environments in place of `nix-cli-ccacheStdenv`. ## Editor integration @@ -261,7 +257,8 @@ See [supported compilation environments](#compilation-environments) and instruct To use the LSP with your editor, you will want a `compile_commands.json` file telling `clangd` how we are compiling the code. Meson's configure always produces this inside the build directory. -Configure your editor to use the `clangd` from the `.#native-clangStdenvPackages` shell. You can do that either by running it inside the development shell, or by using [nix-direnv](https://github.com/nix-community/nix-direnv) and [the appropriate editor plugin](https://github.com/direnv/direnv/wiki#editor-integration). +Configure your editor to use the `clangd` from the `.#native-clangStdenv` shell. +You can do that either by running it inside the development shell, or by using [nix-direnv](https://github.com/nix-community/nix-direnv) and [the appropriate editor plugin](https://github.com/direnv/direnv/wiki#editor-integration). > **Note** > @@ -277,6 +274,8 @@ You may run the formatters as a one-off using: ./maintainers/format.sh ``` +### Pre-commit hooks + If you'd like to run the formatters before every commit, install the hooks: ``` @@ -291,3 +290,30 @@ If it fails, run `git add --patch` to approve the suggestions _and commit again_ To refresh pre-commit hook's config file, do the following: 1. Exit the development shell and start it again by running `nix develop`. 2. If you also use the pre-commit hook, also run `pre-commit-hooks-install` again. + +### VSCode + +Insert the following json into your `.vscode/settings.json` file to configure `nixfmt`. +This will be picked up by the _Format Document_ command, `"editor.formatOnSave"`, etc. + +```json +{ + "nix.formatterPath": "nixfmt", + "nix.serverSettings": { + "nixd": { + "formatting": { + "command": [ + "nixfmt" + ], + }, + }, + "nil": { + "formatting": { + "command": [ + "nixfmt" + ], + }, + }, + }, +} +``` diff --git a/doc/manual/source/development/cli-guideline.md b/doc/manual/source/development/cli-guideline.md index 23df844ec11..052b4aae8e2 100644 --- a/doc/manual/source/development/cli-guideline.md +++ b/doc/manual/source/development/cli-guideline.md @@ -170,9 +170,9 @@ sensitive. ```shell -$ nix init --template=template#pyton +$ nix init --template=template#python ------------------------------------------------------------------------ - Error! Template `template#pyton` not found. + Error! Template `template#python` not found. ------------------------------------------------------------------------ Initializing Nix project at `/path/to/here`. Select a template for you new project: diff --git a/doc/manual/source/development/contributing.md b/doc/manual/source/development/contributing.md index 7de7489dcb7..5c32aeb712a 100644 --- a/doc/manual/source/development/contributing.md +++ b/doc/manual/source/development/contributing.md @@ -20,8 +20,9 @@ prs: 1238 Here's one or more paragraphs that describe the change. - It's markdown -- Add references to the manual using @docroot@ +- Add references to the manual using [links like this](@_at_docroot@/example.md) ``` + Significant changes should add the following header, which moves them to the top. diff --git a/doc/manual/source/development/debugging.md b/doc/manual/source/development/debugging.md index ce623110b36..98456841af1 100644 --- a/doc/manual/source/development/debugging.md +++ b/doc/manual/source/development/debugging.md @@ -2,6 +2,8 @@ This section shows how to build and debug Nix with debug symbols enabled. +Additionally, see [Testing Nix](./testing.md) for further instructions on how to debug Nix in the context of a unit test or functional test. + ## Building Nix with Debug Symbols In the development shell, set the `mesonBuildType` environment variable to `debug` before configuring the build: @@ -13,6 +15,15 @@ In the development shell, set the `mesonBuildType` environment variable to `debu Then, proceed to build Nix as described in [Building Nix](./building.md). This will build Nix with debug symbols, which are essential for effective debugging. +It is also possible to build without debugging for faster build: + +```console +[nix-shell]$ NIX_HARDENING_ENABLE=$(printLines $NIX_HARDENING_ENABLE | grep -v fortify) +[nix-shell]$ export mesonBuildType=debug +``` + +(The first line is needed because `fortify` hardening requires at least some optimization.) + ## Debugging the Nix Binary Obtain your preferred debugger within the development shell: diff --git a/doc/manual/source/development/documentation.md b/doc/manual/source/development/documentation.md index 2e188f23246..30cc8adc44a 100644 --- a/doc/manual/source/development/documentation.md +++ b/doc/manual/source/development/documentation.md @@ -19,10 +19,11 @@ nix-build -E '(import ./.).packages.${builtins.currentSystem}.nix.doc' or ```console -nix build .#nix^doc +nix build .#nix-manual ``` -and open `./result-doc/share/doc/nix/manual/index.html`. +and open `./result/share/doc/nix/manual/index.html`. + To build the manual incrementally, [enter the development shell](./building.md) and run: diff --git a/doc/manual/source/development/testing.md b/doc/manual/source/development/testing.md index 30aa7d0d51b..c0b13015562 100644 --- a/doc/manual/source/development/testing.md +++ b/doc/manual/source/development/testing.md @@ -30,7 +30,7 @@ The unit tests are defined using the [googletest] and [rapidcheck] frameworks. > src > ├── libexpr > │ ├── meson.build -> │ ├── value/context.hh +> │ ├── include/nix/expr/value/context.hh > │ ├── value/context.cc > │ … > │ @@ -46,8 +46,12 @@ The unit tests are defined using the [googletest] and [rapidcheck] frameworks. > │ │ > │ ├── libexpr-test-support > │ │ ├── meson.build +> │ │ ├── include/nix/expr +> │ │ │ ├── meson.build +> │ │ │ └── tests +> │ │ │ ├── value/context.hh +> │ │ │ … > │ │ └── tests -> │ │ ├── value/context.hh > │ │ ├── value/context.cc > │ │ … > │ │ @@ -59,7 +63,7 @@ The unit tests are defined using the [googletest] and [rapidcheck] frameworks. > ``` The tests for each Nix library (`libnixexpr`, `libnixstore`, etc..) live inside a directory `src/${library_name_without-nix}-test`. -Given an interface (header) and implementation pair in the original library, say, `src/libexpr/value/context.{hh,cc}`, we write tests for it in `src/libexpr-tests/value/context.cc`, and (possibly) declare/define additional interfaces for testing purposes in `src/libexpr-test-support/tests/value/context.{hh,cc}`. +Given an interface (header) and implementation pair in the original library, say, `src/libexpr/include/nix/expr/value/context.hh` and `src/libexpr/value/context.cc`, we write tests for it in `src/libexpr-tests/value/context.cc`, and (possibly) declare/define additional interfaces for testing purposes in `src/libexpr-test-support/include/nix/expr/tests/value/context.hh` and `src/libexpr-test-support/tests/value/context.cc`. Data for unit tests is stored in a `data` subdir of the directory for each unit test executable. For example, `libnixstore` code is in `src/libstore`, and its test data is in `src/libstore-tests/data`. @@ -67,7 +71,7 @@ The path to the `src/${library_name_without-nix}-test/data` directory is passed Note that each executable only gets the data for its tests. The unit test libraries are in `src/${library_name_without-nix}-test-support`. -All headers are in a `tests` subdirectory so they are included with `#include "tests/"`. +All headers are in a `tests` subdirectory so they are included with `#include "nix/tests/"`. The use of all these separate directories for the unit tests might seem inconvenient, as for example the tests are not "right next to" the part of the code they are testing. But organizing the tests this way has one big benefit: @@ -87,7 +91,11 @@ A environment variables that Google Test accepts are also worth knowing: This is used to avoid logging passing tests. -Putting the two together, one might run +3. [`GTEST_BREAK_ON_FAILURE`](https://google.github.io/googletest/advanced.html#turning-assertion-failures-into-break-points) + + This is used to create a debugger breakpoint when an assertion failure occurs. + +Putting the first two together, one might run ```bash GTEST_BRIEF=1 GTEST_FILTER='ErrorTraceTest.*' meson test nix-expr-tests -v @@ -95,6 +103,22 @@ GTEST_BRIEF=1 GTEST_FILTER='ErrorTraceTest.*' meson test nix-expr-tests -v for short but comprensive output. +### Debugging tests + +For debugging, it is useful to combine the third option above with Meson's [`--gdb`](https://mesonbuild.com/Unit-tests.html#other-test-options) flag: + +```bash +GTEST_BRIEF=1 GTEST_FILTER='Group.my-failing-test' meson test nix-expr-tests --gdb +``` + +This will: + +1. Run the unit test with GDB + +2. Run just `Group.my-failing-test` + +3. Stop the program when the test fails, allowing the user to then issue arbitrary commands to GDB. + ### Characterisation testing { #characaterisation-testing-unit } See [functional characterisation testing](#characterisation-testing-functional) for a broader discussion of characterisation testing. @@ -144,7 +168,7 @@ $ checkPhase Sometimes it is useful to group related tests so they can be easily run together without running the entire test suite. Each test group is in a subdirectory of `tests`. -For example, `tests/functional/ca/meson.build` defines a `ca` test group for content-addressed derivation outputs. +For example, `tests/functional/ca/meson.build` defines a `ca` test group for content-addressing derivation outputs. That test group can be run like this: @@ -213,10 +237,10 @@ edit it like so: bar ``` -Then, running the test with `./mk/debug-test.sh` will drop you into GDB once the script reaches that point: +Then, running the test with [`--interactive`](https://mesonbuild.com/Unit-tests.html#other-test-options) will prevent Meson from hijacking the terminal so you can drop you into GDB once the script reaches that point: ```shell-session -$ ./mk/debug-test.sh tests/functional/${testName}.sh +$ meson test ${testName} --interactive ... + gdb blash blub GNU gdb (GDB) 12.1 @@ -297,7 +321,7 @@ Creating a Cachix cache for your installer tests and adding its authorisation to - `armv7l-linux` - `x86_64-darwin` -- The `installer_test` job (which runs on `ubuntu-latest` and `macos-latest`) will try to install Nix with the cached installer and run a trivial Nix command. +- The `installer_test` job (which runs on `ubuntu-24.04` and `macos-14`) will try to install Nix with the cached installer and run a trivial Nix command. ### One-time setup diff --git a/doc/manual/source/glossary.md b/doc/manual/source/glossary.md index fa357ece3d6..e6a294e7de7 100644 --- a/doc/manual/source/glossary.md +++ b/doc/manual/source/glossary.md @@ -1,5 +1,13 @@ # Glossary +- [build system]{#gloss-build-system} + + Generic term for software that facilitates the building of software by automating the invocation of compilers, linkers, and other tools. + + Nix can be used as a generic build system. + It has no knowledge of any particular programming language or toolchain. + These details are specified in [derivation expressions](#gloss-derivation-expression). + - [content address]{#gloss-content-address} A @@ -13,37 +21,62 @@ - [Content-Addressing File System Objects](@docroot@/store/file-system-object/content-address.md) - [Content-Addressing Store Objects](@docroot@/store/store-object/content-address.md) - - [content-addressed derivation](#gloss-content-addressed-derivation) + - [content-addressing derivation](#gloss-content-addressing-derivation) Software Heritage's writing on [*Intrinsic and Extrinsic identifiers*](https://www.softwareheritage.org/2020/07/09/intrinsic-vs-extrinsic-identifiers) is also a good introduction to the value of content-addressing over other referencing schemes. Besides content addressing, the Nix store also uses [input addressing](#gloss-input-addressed-store-object). +- [content-addressed storage]{#gloss-content-addressed-store} + + The industry term for storage and retrieval systems using [content addressing](#gloss-content-address). A Nix store also has [input addressing](#gloss-input-addressed-store-object), and metadata. + - [derivation]{#gloss-derivation} - A description of a build task. The result of a derivation is a - store object. Derivations declared in Nix expressions are specified - using the [`derivation` primitive](./language/derivations.md). These are - translated into low-level *store derivations* (implicitly by - `nix-build`, or explicitly by `nix-instantiate`). + A derivation can be thought of as a [pure function](https://en.wikipedia.org/wiki/Pure_function) that produces new [store objects][store object] from existing store objects. + + Derivations are implemented as [operating system processes that run in a sandbox](@docroot@/store/building.md#builder-execution). + This sandbox by default only allows reading from store objects specified as inputs, and only allows writing to designated [outputs][output] to be [captured as store objects](@docroot@/store/building.md#processing-outputs). + + A derivation is typically specified as a [derivation expression] in the [Nix language], and [instantiated][instantiate] to a [store derivation]. + There are multiple ways of obtaining store objects from store derivatons, collectively called [realisation][realise]. [derivation]: #gloss-derivation - [store derivation]{#gloss-store-derivation} - A [derivation] represented as a `.drv` file in the [store]. - It has a [store path], like any [store object]. - It is the [instantiated][instantiate] form of a derivation. - - Example: `/nix/store/g946hcz4c8mdvq2g8vxx42z51qb71rvp-git-2.38.1.drv` + A [derivation] represented as a [store object]. - See [`nix derivation show`](./command-ref/new-cli/nix3-derivation-show.md) (experimental) for displaying the contents of store derivations. + See [Store Derivation](@docroot@/store/derivation/index.md#store-derivation) for details. [store derivation]: #gloss-store-derivation +- [directed acyclic graph]{#gloss-directed-acyclic-graph} + + A [directed acyclic graph](https://en.wikipedia.org/wiki/Directed_acyclic_graph) (DAG) is graph whose edges are given a direction ("a to b" is not the same edge as "b to a"), and for which no possible path (created by joining together edges) forms a cycle. + + DAGs are very important to Nix. + In particular, the non-self-[references][reference] of [store object][store object] form a cycle. + +- [derivation path]{#gloss-derivation-path} + + A [store path] which uniquely identifies a [store derivation]. + + See [Referencing Store Derivations](@docroot@/store/derivation/index.md#derivation-path) for details. + + Not to be confused with [deriving path]. + + [derivation path]: #gloss-derivation-path + +- [derivation expression]{#gloss-derivation-expression} + + A description of a [store derivation] using the [`derivation` primitive](./language/derivations.md) in the [Nix language]. + + [derivation expression]: #gloss-derivation-expression + - [instantiate]{#gloss-instantiate}, instantiation - Save an evaluated [derivation] as a [store derivation] in the Nix [store]. + Translate a [derivation expression] into a [store derivation]. See [`nix-instantiate`](./command-ref/nix-instantiate.md), which produces a store derivation from a Nix expression that evaluates to a derivation. @@ -55,9 +88,8 @@ This can be achieved by: - Fetching a pre-built [store object] from a [substituter] - - Running the [`builder`](@docroot@/language/derivations.md#attr-builder) executable as specified in the corresponding [derivation] + - [Building](@docroot@/store/building.md) the corresponding [store derivation] - Delegating to a [remote machine](@docroot@/command-ref/conf-file.md#conf-builders) and retrieving the outputs - See [`nix-store --realise`](@docroot@/command-ref/nix-store/realise.md) for a detailed description of the algorithm. @@ -65,7 +97,7 @@ [realise]: #gloss-realise -- [content-addressed derivation]{#gloss-content-addressed-derivation} +- [content-addressing derivation]{#gloss-content-addressing-derivation} A derivation which has the [`__contentAddressed`](./language/advanced-attributes.md#adv-attr-__contentAddressed) @@ -73,7 +105,7 @@ - [fixed-output derivation]{#gloss-fixed-output-derivation} (FOD) - A [derivation] where a cryptographic hash of the [output] is determined in advance using the [`outputHash`](./language/advanced-attributes.md#adv-attr-outputHash) attribute, and where the [`builder`](@docroot@/language/derivations.md#attr-builder) executable has access to the network. + A [store derivation] where a cryptographic hash of the [output] is determined in advance using the [`outputHash`](./language/advanced-attributes.md#adv-attr-outputHash) attribute, and where the [`builder`](@docroot@/language/derivations.md#attr-builder) executable has access to the network. - [store]{#gloss-store} @@ -84,6 +116,12 @@ [store]: #gloss-store +- [Nix instance]{#gloss-nix-instance} + + 1. An installation of Nix, which includes the presence of a [store], and the Nix package manager which operates on that store. + A local Nix installation and a [remote builder](@docroot@/advanced-topics/distributed-builds.md) are two examples of Nix instances. + 2. A running Nix process, such as the `nix` command. + - [binary cache]{#gloss-binary-cache} A *binary cache* is a Nix store which uses a different format: its @@ -130,15 +168,17 @@ - [input-addressed store object]{#gloss-input-addressed-store-object} A store object produced by building a - non-[content-addressed](#gloss-content-addressed-derivation), + non-[content-addressed](#gloss-content-addressing-derivation), non-[fixed-output](#gloss-fixed-output-derivation) derivation. + See [input-addressing derivation outputs](store/derivation/outputs/input-address.md) for details. + - [content-addressed store object]{#gloss-content-addressed-store-object} A [store object] which is [content-addressed](#gloss-content-address), i.e. whose [store path] is determined by its contents. - This includes derivations, the outputs of [content-addressed derivations](#gloss-content-addressed-derivation), and the outputs of [fixed-output derivations](#gloss-fixed-output-derivation). + This includes derivations, the outputs of [content-addressing derivations](#gloss-content-addressing-derivation), and the outputs of [fixed-output derivations](#gloss-fixed-output-derivation). See [Content-Addressing Store Objects](@docroot@/store/store-object/content-address.md) for details. @@ -188,35 +228,37 @@ > > The contents of a `.nix` file form a Nix expression. - Nix expressions specify [derivations][derivation], which are [instantiated][instantiate] into the Nix store as [store derivations][store derivation]. + Nix expressions specify [derivation expressions][derivation expression], which are [instantiated][instantiate] into the Nix store as [store derivations][store derivation]. These derivations can then be [realised][realise] to produce [outputs][output]. > **Example** > - > Building and deploying software using Nix entails writing Nix expressions as a high-level description of packages and compositions thereof. + > Building and deploying software using Nix entails writing Nix expressions to describe [packages][package] and compositions thereof. - [reference]{#gloss-reference} - A [store object] `O` is said to have a *reference* to a store object `P` if a [store path] to `P` appears in the contents of `O`. + An edge from one [store object] to another. - Store objects can refer to both other store objects and themselves. - References from a store object to itself are called *self-references*. - References other than a self-reference must not form a cycle. + See [References](@docroot@/store/store-object.md#references) for details. [reference]: #gloss-reference + See [References](@docroot@/store/store-object.md#references) for details. + - [reachable]{#gloss-reachable} A store path `Q` is reachable from another store path `P` if `Q` is in the *closure* of the *references* relation. + See [References](@docroot@/store/store-object.md#references) for details. + - [closure]{#gloss-closure} The closure of a store path is the set of store paths that are directly or indirectly “reachable†from that store path; that is, it’s the closure of the path under the *references* relation. For a package, the closure of its derivation is equivalent to the - build-time dependencies, while the closure of its output path is + build-time dependencies, while the closure of its [output path] is equivalent to its runtime dependencies. For correct deployment it is necessary to deploy whole closures, since otherwise at runtime files could be missing. The command `nix-store --query --requisites ` prints out @@ -226,18 +268,31 @@ to a store object at path `Q`, then `Q` is in the closure of `P`. Further, if `Q` references `R` then `R` is also in the closure of `P`. + See [References](@docroot@/store/store-object.md#references) for details. + [closure]: #gloss-closure +- [requisite]{#gloss-requisite} + + A store object [reachable] by a path (chain of references) from a given [store object]. + The [closure] is the set of requisites. + + See [References](@docroot@/store/store-object.md#references) for details. + +- [referrer]{#gloss-reference} + + A reversed edge from one [store object] to another. + - [output]{#gloss-output} - A [store object] produced by a [derivation]. + A [store object] produced by a [store derivation]. See [the `outputs` argument to the `derivation` function](@docroot@/language/derivations.md#attr-outputs) for details. [output]: #gloss-output - [output path]{#gloss-output-path} - The [store path] to the [output] of a [derivation]. + The [store path] to the [output] of a [store derivation]. [output path]: #gloss-output-path @@ -246,14 +301,11 @@ - [deriving path]{#gloss-deriving-path} - Deriving paths are a way to refer to [store objects][store object] that ar not yet [realised][realise]. - This is necessary because, in general and particularly for [content-addressed derivations][content-addressed derivation], the [output path] of an [output] is not known in advance. - There are two forms: + Deriving paths are a way to refer to [store objects][store object] that might not yet be [realised][realise]. - - *constant*: just a [store path] - It can be made [valid][validity] by copying it into the store: from the evaluator, command line interface or another store. + See [Deriving Path](./store/derivation/index.md#deriving-path) for details. - - *output*: a pair of a [store path] to a [derivation] and an [output] name. + Not to be confused with [derivation path]. - [deriver]{#gloss-deriver} @@ -301,7 +353,7 @@ See [Nix Archive](store/file-system-object/content-address.html#serial-nix-archive) for details. -- [`∅`]{#gloss-emtpy-set} +- [`∅`]{#gloss-empty-set} The empty set symbol. In the context of profile history, this denotes a package is not present in a particular version of the profile. @@ -311,18 +363,17 @@ - [package]{#package} - 1. A software package; a collection of files and other data. + A software package; files that belong together for a particular purpose, and metadata. - 2. A [package attribute set]. + Nix represents files as [file system objects][file system object], and how they belong together is encoded as [references][reference] between [store objects][store object] that contain these file system objects. -- [package attribute set]{#package-attribute-set} + The [Nix language] allows denoting packages in terms of [attribute sets](@docroot@/language/types.md#attribute-set) containing: + - attributes that refer to the files of a package, typically in the form of [derivation outputs](#output), + - attributes with metadata, such as information about how the package is supposed to be used. - An [attribute set](@docroot@/language/types.md#attribute-set) containing the attribute `type = "derivation";` (derivation for historical reasons), as well as other attributes, such as - - attributes that refer to the files of a [package], typically in the form of [derivation outputs](#output), - - attributes that declare something about how the package is supposed to be installed or used, - - other metadata or arbitrary attributes. + The exact shape of these attribute sets is up to convention. - [package attribute set]: #package-attribute-set + [package]: #package - [string interpolation]{#gloss-string-interpolation} diff --git a/doc/manual/source/installation/index.md b/doc/manual/source/installation/index.md index 48725c1ba74..3c09f103184 100644 --- a/doc/manual/source/installation/index.md +++ b/doc/manual/source/installation/index.md @@ -30,6 +30,8 @@ $ curl -L https://nixos.org/nix/install | sh -s -- --daemon > Single-user is not supported on Mac. +> `warning: installing Nix as root is not supported by this script!` + This installation has less requirements than the multi-user install, however it cannot offer equivalent sharing, isolation, or security. diff --git a/doc/manual/source/installation/installing-binary.md b/doc/manual/source/installation/installing-binary.md index 6a1a5ddcaff..21c15637437 100644 --- a/doc/manual/source/installation/installing-binary.md +++ b/doc/manual/source/installation/installing-binary.md @@ -25,7 +25,7 @@ This performs the default type of installation for your platform: We recommend the multi-user installation if it supports your platform and you can authenticate with `sudo`. -The installer can configured with various command line arguments and environment variables. +The installer can be configured with various command line arguments and environment variables. To show available command line flags: ```console diff --git a/doc/manual/source/installation/uninstall.md b/doc/manual/source/installation/uninstall.md index 47689a16ece..8d45da6bba0 100644 --- a/doc/manual/source/installation/uninstall.md +++ b/doc/manual/source/installation/uninstall.md @@ -160,6 +160,6 @@ which you may remove. To remove a [single-user installation](./installing-binary.md#single-user-installation) of Nix, run: ```console -$ rm -rf /nix ~/.nix-channels ~/.nix-defexpr ~/.nix-profile +rm -rf /nix ~/.nix-channels ~/.nix-defexpr ~/.nix-profile ``` You might also want to manually remove references to Nix from your `~/.profile`. diff --git a/doc/manual/source/introduction.md b/doc/manual/source/introduction.md index 76489bc1b2c..e70411c11f5 100644 --- a/doc/manual/source/introduction.md +++ b/doc/manual/source/introduction.md @@ -1,8 +1,8 @@ # Introduction Nix is a _purely functional package manager_. This means that it -treats packages like values in purely functional programming languages -such as Haskell — they are built by functions that don’t have +treats packages like values in a purely functional programming language +— packages are built by functions that don’t have side-effects, and they never change after they have been built. Nix stores packages in the _Nix store_, usually the directory `/nix/store`, where each package has its own unique subdirectory such diff --git a/doc/manual/source/language/advanced-attributes.md b/doc/manual/source/language/advanced-attributes.md index 51b83fc8acc..34c3b636b39 100644 --- a/doc/manual/source/language/advanced-attributes.md +++ b/doc/manual/source/language/advanced-attributes.md @@ -2,6 +2,75 @@ Derivations can declare some infrequently used optional attributes. +## Inputs + + - [`exportReferencesGraph`]{#adv-attr-exportReferencesGraph}\ + This attribute allows builders access to the references graph of + their inputs. The attribute is a list of inputs in the Nix store + whose references graph the builder needs to know. The value of + this attribute should be a list of pairs `[ name1 path1 name2 + path2 ... ]`. The references graph of each *pathN* will be stored + in a text file *nameN* in the temporary build directory. The text + files have the format used by `nix-store --register-validity` + (with the deriver fields left empty). For example, when the + following derivation is built: + + ```nix + derivation { + ... + exportReferencesGraph = [ "libfoo-graph" libfoo ]; + }; + ``` + + the references graph of `libfoo` is placed in the file + `libfoo-graph` in the temporary build directory. + + `exportReferencesGraph` is useful for builders that want to do + something with the closure of a store path. Examples include the + builders in NixOS that generate the initial ramdisk for booting + Linux (a `cpio` archive containing the closure of the boot script) + and the ISO-9660 image for the installation CD (which is populated + with a Nix store containing the closure of a bootable NixOS + configuration). + + - [`passAsFile`]{#adv-attr-passAsFile}\ + A list of names of attributes that should be passed via files rather + than environment variables. For example, if you have + + ```nix + passAsFile = ["big"]; + big = "a very long string"; + ``` + + then when the builder runs, the environment variable `bigPath` + will contain the absolute path to a temporary file containing `a + very long string`. That is, for any attribute *x* listed in + `passAsFile`, Nix will pass an environment variable `xPath` + holding the path of the file containing the value of attribute + *x*. This is useful when you need to pass large strings to a + builder, since most operating systems impose a limit on the size + of the environment (typically, a few hundred kilobyte). + + - [`__structuredAttrs`]{#adv-attr-structuredAttrs}\ + If the special attribute `__structuredAttrs` is set to `true`, the other derivation + attributes are serialised into a file in JSON format. + + This obviates the need for [`passAsFile`](#adv-attr-passAsFile) since JSON files have no size restrictions, unlike process environments. + It also makes it possible to tweak derivation settings in a structured way; + see [`outputChecks`](#adv-attr-outputChecks) for example. + + See the [corresponding section in the derivation page](@docroot@/store/derivation/index.md#structured-attrs) for further details. + + > **Warning** + > + > If set to `true`, other advanced attributes such as [`allowedReferences`](#adv-attr-allowedReferences), [`allowedRequisites`](#adv-attr-allowedRequisites), + [`disallowedReferences`](#adv-attr-disallowedReferences) and [`disallowedRequisites`](#adv-attr-disallowedRequisites), maxSize, and maxClosureSize. + will have no effect. + +## Output checks + +See the [corresponding section in the derivation output page](@docroot@/store/derivation/outputs/index.md). + - [`allowedReferences`]{#adv-attr-allowedReferences}\ The optional attribute `allowedReferences` specifies a list of legal references (dependencies) of the output of the builder. For example, @@ -55,34 +124,89 @@ Derivations can declare some infrequently used optional attributes. dependency on `foobar` or any other derivation depending recursively on `foobar`. - - [`exportReferencesGraph`]{#adv-attr-exportReferencesGraph}\ - This attribute allows builders access to the references graph of - their inputs. The attribute is a list of inputs in the Nix store - whose references graph the builder needs to know. The value of - this attribute should be a list of pairs `[ name1 path1 name2 - path2 ... ]`. The references graph of each *pathN* will be stored - in a text file *nameN* in the temporary build directory. The text - files have the format used by `nix-store --register-validity` - (with the deriver fields left empty). For example, when the - following derivation is built: + - [`outputChecks`]{#adv-attr-outputChecks}\ + When using [structured attributes](#adv-attr-structuredAttrs), the `outputChecks` + attribute allows defining checks per-output. + + In addition to + [`allowedReferences`](#adv-attr-allowedReferences), [`allowedRequisites`](#adv-attr-allowedRequisites), + [`disallowedReferences`](#adv-attr-disallowedReferences) and [`disallowedRequisites`](#adv-attr-disallowedRequisites), + the following attributes are available: + + - `maxSize` defines the maximum size of the resulting [store object](@docroot@/store/store-object.md). + - `maxClosureSize` defines the maximum size of the output's closure. + - `ignoreSelfRefs` controls whether self-references should be considered when + checking for allowed references/requisites. + + Example: ```nix - derivation { - ... - exportReferencesGraph = [ "libfoo-graph" libfoo ]; + __structuredAttrs = true; + + outputChecks.out = { + # The closure of 'out' must not be larger than 256 MiB. + maxClosureSize = 256 * 1024 * 1024; + + # It must not refer to the C compiler or to the 'dev' output. + disallowedRequisites = [ stdenv.cc "dev" ]; + }; + + outputChecks.dev = { + # The 'dev' output must not be larger than 128 KiB. + maxSize = 128 * 1024; }; ``` - the references graph of `libfoo` is placed in the file - `libfoo-graph` in the temporary build directory. +## Other output modifications - `exportReferencesGraph` is useful for builders that want to do - something with the closure of a store path. Examples include the - builders in NixOS that generate the initial ramdisk for booting - Linux (a `cpio` archive containing the closure of the boot script) - and the ISO-9660 image for the installation CD (which is populated - with a Nix store containing the closure of a bootable NixOS - configuration). + - [`unsafeDiscardReferences`]{#adv-attr-unsafeDiscardReferences}\ + + When using [structured attributes](#adv-attr-structuredAttrs), the + attribute `unsafeDiscardReferences` is an attribute set with a boolean value for each output name. + If set to `true`, it disables scanning the output for runtime dependencies. + + Example: + + ```nix + __structuredAttrs = true; + unsafeDiscardReferences.out = true; + ``` + + This is useful, for example, when generating self-contained filesystem images with + their own embedded Nix store: hashes found inside such an image refer + to the embedded store and not to the host's Nix store. + +## Build scheduling + + - [`preferLocalBuild`]{#adv-attr-preferLocalBuild}\ + If this attribute is set to `true` and [distributed building is enabled](@docroot@/command-ref/conf-file.md#conf-builders), then, if possible, the derivation will be built locally instead of being forwarded to a remote machine. + This is useful for derivations that are cheapest to build locally. + + - [`allowSubstitutes`]{#adv-attr-allowSubstitutes}\ + If this attribute is set to `false`, then Nix will always build this derivation (locally or remotely); it will not try to substitute its outputs. + This is useful for derivations that are cheaper to build than to substitute. + + This attribute can be ignored by setting [`always-allow-substitutes`](@docroot@/command-ref/conf-file.md#conf-always-allow-substitutes) to `true`. + + > **Note** + > + > If set to `false`, the [`builder`] should be able to run on the system type specified in the [`system` attribute](./derivations.md#attr-system), since the derivation cannot be substituted. + + [`builder`]: ./derivations.md#attr-builder + +- [`requiredSystemFeatures`]{#adv-attr-requiredSystemFeatures}\ + + If a derivation has the `requiredSystemFeatures` attribute, then Nix will only build it on a machine that has the corresponding features set in its [`system-features` configuration](@docroot@/command-ref/conf-file.md#conf-system-features). + + For example, setting + + ```nix + requiredSystemFeatures = [ "kvm" ]; + ``` + + ensures that the derivation can only be built on a machine with the `kvm` feature. + +# Impure builder configuration - [`impureEnvVars`]{#adv-attr-impureEnvVars}\ This attribute allows you to specify a list of environment variables @@ -99,8 +223,8 @@ Derivations can declare some infrequently used optional attributes. to make it use the proxy server configuration specified by the user in the environment variables `http_proxy` and friends. - This attribute is only allowed in *fixed-output derivations* (see - below), where impurities such as these are okay since (the hash + This attribute is only allowed in [fixed-output derivations][fixed-output derivation], + where impurities such as these are okay since (the hash of) the output is known in advance. It is ignored for all other derivations. @@ -119,81 +243,55 @@ Derivations can declare some infrequently used optional attributes. [`impure-env`](@docroot@/command-ref/conf-file.md#conf-impure-env) configuration setting. - - [`outputHash`]{#adv-attr-outputHash}; [`outputHashAlgo`]{#adv-attr-outputHashAlgo}; [`outputHashMode`]{#adv-attr-outputHashMode}\ - These attributes declare that the derivation is a so-called *fixed-output derivation* (FOD), which means that a cryptographic hash of the output is already known in advance. +## Setting the derivation type - As opposed to regular derivations, the [`builder`] executable of a fixed-output derivation has access to the network. - Nix computes a cryptographic hash of its output and compares that to the hash declared with these attributes. - If there is a mismatch, the derivation fails. +As discussed in [Derivation Outputs and Types of Derivations](@docroot@/store/derivation/outputs/index.md), there are multiples kinds of derivations / kinds of derivation outputs. +The choice of the following attributes determines which kind of derivation we are making. - The rationale for fixed-output derivations is derivations such as - those produced by the `fetchurl` function. This function downloads a - file from a given URL. To ensure that the downloaded file has not - been modified, the caller must also specify a cryptographic hash of - the file. For example, +- [`__contentAddressed`] - ```nix - fetchurl { - url = "http://ftp.gnu.org/pub/gnu/hello/hello-2.1.1.tar.gz"; - sha256 = "1md7jsfd8pa45z73bz1kszpp01yw6x5ljkjk2hx7wl800any6465"; - } - ``` +- [`outputHash`] - It sometimes happens that the URL of the file changes, e.g., because - servers are reorganised or no longer available. We then must update - the call to `fetchurl`, e.g., +- [`outputHashAlgo`] - ```nix - fetchurl { - url = "ftp://ftp.nluug.nl/pub/gnu/hello/hello-2.1.1.tar.gz"; - sha256 = "1md7jsfd8pa45z73bz1kszpp01yw6x5ljkjk2hx7wl800any6465"; - } - ``` +- [`outputHashMode`] - If a `fetchurl` derivation was treated like a normal derivation, the - output paths of the derivation and *all derivations depending on it* - would change. For instance, if we were to change the URL of the - Glibc source distribution in Nixpkgs (a package on which almost all - other packages depend) massive rebuilds would be needed. This is - unfortunate for a change which we know cannot have a real effect as - it propagates upwards through the dependency graph. +The three types of derivations are chosen based on the following combinations of these attributes. +All other combinations are invalid. - For fixed-output derivations, on the other hand, the name of the - output path only depends on the `outputHash*` and `name` attributes, - while all other attributes are ignored for the purpose of computing - the output path. (The `name` attribute is included because it is - part of the path.) +- [Input-addressing derivations](@docroot@/store/derivation/outputs/input-address.md) - As an example, here is the (simplified) Nix expression for - `fetchurl`: + This is the default for `builtins.derivation`. + Nix only currently supports one kind of input-addressing, so no other information is needed. - ```nix - { stdenv, curl }: # The curl program is used for downloading. + `__contentAddressed = false;` may also be included, but is not needed, and will trigger the experimental feature check. - { url, sha256 }: +- [Fixed-output derivations][fixed-output derivation] - stdenv.mkDerivation { - name = baseNameOf (toString url); - builder = ./builder.sh; - buildInputs = [ curl ]; + All of [`outputHash`], [`outputHashAlgo`], and [`outputHashMode`]. - # This is a fixed-output derivation; the output must be a regular - # file with SHA256 hash sha256. - outputHashMode = "flat"; - outputHashAlgo = "sha256"; - outputHash = sha256; + + +- [(Floating) content-addressing derivations](@docroot@/store/derivation/outputs/content-address.md) + + Both [`outputHashAlgo`] and [`outputHashMode`], `__contentAddressed = true;`, and *not* `outputHash`. + + If an output hash was given, then the derivation output would be "fixed" not "floating". + +Here is more information on the `output*` attributes, and what values they may be set to: + + - [`outputHashMode`]{#adv-attr-outputHashMode} + + This specifies how the files of a content-addressing derivation output are digested to produce a content address. + + This works in conjunction with [`outputHashAlgo`](#adv-attr-outputHashAlgo). + Specifying one without the other is an error (unless [`outputHash` is also specified and includes its own hash algorithm as described below). The `outputHashMode` attribute determines how the hash is computed. It must be one of the following values: @@ -223,153 +321,56 @@ Derivations can declare some infrequently used optional attributes. > > This method is part of the [`git-hashing`][xp-feature-git-hashing] experimental feature. - - [`__contentAddressed`]{#adv-attr-__contentAddressed} - - > **Warning** - > This attribute is part of an [experimental feature](@docroot@/development/experimental-features.md). - > - > To use this attribute, you must enable the - > [`ca-derivations`][xp-feature-ca-derivations] experimental feature. - > For example, in [nix.conf](../command-ref/conf-file.md) you could add: - > - > ``` - > extra-experimental-features = ca-derivations - > ``` - - If this attribute is set to `true`, then the derivation - outputs will be stored in a content-addressed location rather than the - traditional input-addressed one. + See [content-addressing store objects](@docroot@/store/store-object/content-address.md) for more information about the process this flag controls. - Setting this attribute also requires setting - [`outputHashMode`](#adv-attr-outputHashMode) - and - [`outputHashAlgo`](#adv-attr-outputHashAlgo) - like for *fixed-output derivations* (see above). + - [`outputHashAlgo`]{#adv-attr-outputHashAlgo} - It also implicitly requires that the machine to build the derivation must have the `ca-derivations` [system feature](@docroot@/command-ref/conf-file.md#conf-system-features). + This specifies the hash algorithm used to digest the [file system object] data of a content-addressing derivation output. - - [`passAsFile`]{#adv-attr-passAsFile}\ - A list of names of attributes that should be passed via files rather - than environment variables. For example, if you have + This works in conjunction with [`outputHashMode`](#adv-attr-outputHashAlgo). + Specifying one without the other is an error (unless `outputHash` is also specified and includes its own hash algorithm as described below). - ```nix - passAsFile = ["big"]; - big = "a very long string"; - ``` + The `outputHashAlgo` attribute specifies the hash algorithm used to compute the hash. + It can currently be `"blake3"`, `"sha1"`, `"sha256"`, `"sha512"`, or `null`. - then when the builder runs, the environment variable `bigPath` - will contain the absolute path to a temporary file containing `a - very long string`. That is, for any attribute *x* listed in - `passAsFile`, Nix will pass an environment variable `xPath` - holding the path of the file containing the value of attribute - *x*. This is useful when you need to pass large strings to a - builder, since most operating systems impose a limit on the size - of the environment (typically, a few hundred kilobyte). + `outputHashAlgo` can only be `null` when `outputHash` follows the SRI format, because in that case the choice of hash algorithm is determined by `outputHash`. - - [`preferLocalBuild`]{#adv-attr-preferLocalBuild}\ - If this attribute is set to `true` and [distributed building is enabled](@docroot@/command-ref/conf-file.md#conf-builders), then, if possible, the derivation will be built locally instead of being forwarded to a remote machine. - This is useful for derivations that are cheapest to build locally. + - [`outputHash`]{#adv-attr-outputHashAlgo}; [`outputHash`]{#adv-attr-outputHashMode} - - [`allowSubstitutes`]{#adv-attr-allowSubstitutes}\ - If this attribute is set to `false`, then Nix will always build this derivation (locally or remotely); it will not try to substitute its outputs. - This is useful for derivations that are cheaper to build than to substitute. + This will specify the output hash of the single output of a [fixed-output derivation]. - This attribute can be ignored by setting [`always-allow-substitutes`](@docroot@/command-ref/conf-file.md#conf-always-allow-substitutes) to `true`. + The `outputHash` attribute must be a string containing the hash in either hexadecimal or "nix32" encoding, or following the format for integrity metadata as defined by [SRI](https://www.w3.org/TR/SRI/). + The "nix32" encoding is an adaptation of base-32 encoding. > **Note** > - > If set to `false`, the [`builder`] should be able to run on the system type specified in the [`system` attribute](./derivations.md#attr-system), since the derivation cannot be substituted. + > The [`convertHash`](@docroot@/language/builtins.md#builtins-convertHash) function shows how to convert between different encodings. + > The [`nix-hash` command](../command-ref/nix-hash.md) has information about obtaining the hash for some contents, as well as converting to and from encodings. - [`builder`]: ./derivations.md#attr-builder - - - [`__structuredAttrs`]{#adv-attr-structuredAttrs}\ - If the special attribute `__structuredAttrs` is set to `true`, the other derivation - attributes are serialised into a file in JSON format. The environment variable - `NIX_ATTRS_JSON_FILE` points to the exact location of that file both in a build - and a [`nix-shell`](../command-ref/nix-shell.md). This obviates the need for - [`passAsFile`](#adv-attr-passAsFile) since JSON files have no size restrictions, - unlike process environments. - - It also makes it possible to tweak derivation settings in a structured way; see - [`outputChecks`](#adv-attr-outputChecks) for example. - - As a convenience to Bash builders, - Nix writes a script that initialises shell variables - corresponding to all attributes that are representable in Bash. The - environment variable `NIX_ATTRS_SH_FILE` points to the exact - location of the script, both in a build and a - [`nix-shell`](../command-ref/nix-shell.md). This includes non-nested - (associative) arrays. For example, the attribute `hardening.format = true` - ends up as the Bash associative array element `${hardening[format]}`. + - [`__contentAddressed`]{#adv-attr-__contentAddressed} > **Warning** > - > If set to `true`, other advanced attributes such as [`allowedReferences`](#adv-attr-allowedReferences), [`allowedReferences`](#adv-attr-allowedReferences), [`allowedRequisites`](#adv-attr-allowedRequisites), - [`disallowedReferences`](#adv-attr-disallowedReferences) and [`disallowedRequisites`](#adv-attr-disallowedRequisites), maxSize, and maxClosureSize. - will have no effect. - - - [`outputChecks`]{#adv-attr-outputChecks}\ - When using [structured attributes](#adv-attr-structuredAttrs), the `outputChecks` - attribute allows defining checks per-output. - - In addition to - [`allowedReferences`](#adv-attr-allowedReferences), [`allowedRequisites`](#adv-attr-allowedRequisites), - [`disallowedReferences`](#adv-attr-disallowedReferences) and [`disallowedRequisites`](#adv-attr-disallowedRequisites), - the following attributes are available: - - - `maxSize` defines the maximum size of the resulting [store object](@docroot@/store/store-object.md). - - `maxClosureSize` defines the maximum size of the output's closure. - - `ignoreSelfRefs` controls whether self-references should be considered when - checking for allowed references/requisites. - - Example: - - ```nix - __structuredAttrs = true; - - outputChecks.out = { - # The closure of 'out' must not be larger than 256 MiB. - maxClosureSize = 256 * 1024 * 1024; - - # It must not refer to the C compiler or to the 'dev' output. - disallowedRequisites = [ stdenv.cc "dev" ]; - }; - - outputChecks.dev = { - # The 'dev' output must not be larger than 128 KiB. - maxSize = 128 * 1024; - }; - ``` - - - [`unsafeDiscardReferences`]{#adv-attr-unsafeDiscardReferences}\ - - When using [structured attributes](#adv-attr-structuredAttrs), the - attribute `unsafeDiscardReferences` is an attribute set with a boolean value for each output name. - If set to `true`, it disables scanning the output for runtime dependencies. - - Example: - - ```nix - __structuredAttrs = true; - unsafeDiscardReferences.out = true; - ``` - - This is useful, for example, when generating self-contained filesystem images with - their own embedded Nix store: hashes found inside such an image refer - to the embedded store and not to the host's Nix store. - -- [`requiredSystemFeatures`]{#adv-attr-requiredSystemFeatures}\ - - If a derivation has the `requiredSystemFeatures` attribute, then Nix will only build it on a machine that has the corresponding features set in its [`system-features` configuration](@docroot@/command-ref/conf-file.md#conf-system-features). - - For example, setting + > This attribute is part of an [experimental feature](@docroot@/development/experimental-features.md). + > + > To use this attribute, you must enable the + > [`ca-derivations`][xp-feature-ca-derivations] experimental feature. + > For example, in [nix.conf](../command-ref/conf-file.md) you could add: + > + > ``` + > extra-experimental-features = ca-derivations + > ``` - ```nix - requiredSystemFeatures = [ "kvm" ]; - ``` + This is a boolean with a default of `false`. + It determines whether the derivation is floating content-addressing. - ensures that the derivation can only be built on a machine with the `kvm` feature. +[`__contentAddressed`]: #adv-attr-__contentAddressed +[`outputHash`]: #adv-attr-outputHash +[`outputHashAlgo`]: #adv-attr-outputHashAlgo +[`outputHashMode`]: #adv-attr-outputHashMode -[xp-feature-ca-derivations]: @docroot@/development/experimental-features.md#xp-feature-ca-derivations +[fixed-output derivation]: @docroot@/glossary.md#gloss-fixed-output-derivation +[file system object]: @docroot@/store/file-system-object.md +[store object]: @docroot@/store/store-object.md [xp-feature-dynamic-derivations]: @docroot@/development/experimental-features.md#xp-feature-dynamic-derivations [xp-feature-git-hashing]: @docroot@/development/experimental-features.md#xp-feature-git-hashing diff --git a/doc/manual/source/language/derivations.md b/doc/manual/source/language/derivations.md index 771b2bd9130..43eec680bbc 100644 --- a/doc/manual/source/language/derivations.md +++ b/doc/manual/source/language/derivations.md @@ -1,9 +1,10 @@ # Derivations -The most important built-in function is `derivation`, which is used to describe a single derivation: -a specification for running an executable on precisely defined input files to repeatably produce output files at uniquely determined file system paths. +The most important built-in function is `derivation`, which is used to describe a single store-layer [store derivation]. +Consult the [store chapter](@docroot@/store/derivation/index.md) for what a store derivation is; +this section just concerns how to create one from the Nix language. -It takes as input an attribute set, the attributes of which specify the inputs to the process. +This builtin function takes as input an attribute set, the attributes of which specify the inputs to the process. It outputs an attribute set, and produces a [store derivation] as a side effect of evaluation. [store derivation]: @docroot@/glossary.md#gloss-store-derivation @@ -15,7 +16,7 @@ It outputs an attribute set, and produces a [store derivation] as a side effect - [`name`]{#attr-name} ([String](@docroot@/language/types.md#type-string)) A symbolic name for the derivation. - It is added to the [store path] of the corresponding [store derivation] as well as to its [output paths](@docroot@/glossary.md#gloss-output-path). + See [derivation outputs](@docroot@/store/derivation/index.md#outputs) for what this is affects. [store path]: @docroot@/store/store-path.md @@ -28,17 +29,12 @@ It outputs an attribute set, and produces a [store derivation] as a side effect > } > ``` > - > The store derivation's path will be `/nix/store/-hello.drv`. + > The derivation's path will be `/nix/store/-hello.drv`. > The [output](#attr-outputs) paths will be of the form `/nix/store/-hello[-]` - [`system`]{#attr-system} ([String](@docroot@/language/types.md#type-string)) - The system type on which the [`builder`](#attr-builder) executable is meant to be run. - - A necessary condition for Nix to build derivations locally is that the `system` attribute matches the current [`system` configuration option]. - It can automatically [build on other platforms](@docroot@/language/derivations.md#attr-builder) by forwarding build requests to other machines. - - [`system` configuration option]: @docroot@/command-ref/conf-file.md#conf-system + See [system](@docroot@/store/derivation/index.md#system). > **Example** > @@ -68,7 +64,7 @@ It outputs an attribute set, and produces a [store derivation] as a side effect - [`builder`]{#attr-builder} ([Path](@docroot@/language/types.md#type-path) | [String](@docroot@/language/types.md#type-string)) - Path to an executable that will perform the build. + See [builder](@docroot@/store/derivation/index.md#builder). > **Example** > @@ -117,7 +113,7 @@ It outputs an attribute set, and produces a [store derivation] as a side effect Default: `[ ]` - Command-line arguments to be passed to the [`builder`](#attr-builder) executable. + See [args](@docroot@/store/derivation/index.md#args). > **Example** > @@ -239,77 +235,3 @@ It outputs an attribute set, and produces a [store derivation] as a side effect passed as an empty string. - -## Builder execution - -The [`builder`](#attr-builder) is executed as follows: - -- A temporary directory is created under the directory specified by - `TMPDIR` (default `/tmp`) where the build will take place. The - current directory is changed to this directory. - -- The environment is cleared and set to the derivation attributes, as - specified above. - -- In addition, the following variables are set: - - - `NIX_BUILD_TOP` contains the path of the temporary directory for - this build. - - - Also, `TMPDIR`, `TEMPDIR`, `TMP`, `TEMP` are set to point to the - temporary directory. This is to prevent the builder from - accidentally writing temporary files anywhere else. Doing so - might cause interference by other processes. - - - `PATH` is set to `/path-not-set` to prevent shells from - initialising it to their built-in default value. - - - `HOME` is set to `/homeless-shelter` to prevent programs from - using `/etc/passwd` or the like to find the user's home - directory, which could cause impurity. Usually, when `HOME` is - set, it is used as the location of the home directory, even if - it points to a non-existent path. - - - `NIX_STORE` is set to the path of the top-level Nix store - directory (typically, `/nix/store`). - - - `NIX_ATTRS_JSON_FILE` & `NIX_ATTRS_SH_FILE` if `__structuredAttrs` - is set to `true` for the derivation. A detailed explanation of this - behavior can be found in the - [section about structured attrs](./advanced-attributes.md#adv-attr-structuredAttrs). - - - For each output declared in `outputs`, the corresponding - environment variable is set to point to the intended path in the - Nix store for that output. Each output path is a concatenation - of the cryptographic hash of all build inputs, the `name` - attribute and the output name. (The output name is omitted if - it’s `out`.) - -- If an output path already exists, it is removed. Also, locks are - acquired to prevent multiple Nix instances from performing the same - build at the same time. - -- A log of the combined standard output and error is written to - `/nix/var/log/nix`. - -- The builder is executed with the arguments specified by the - attribute `args`. If it exits with exit code 0, it is considered to - have succeeded. - -- The temporary directory is removed (unless the `-K` option was - specified). - -- If the build was successful, Nix scans each output path for - references to input paths by looking for the hash parts of the input - paths. Since these are potential runtime dependencies, Nix registers - them as dependencies of the output paths. - -- After the build, Nix sets the last-modified timestamp on all files - in the build result to 1 (00:00:01 1/1/1970 UTC), sets the group to - the default group, and sets the mode of the file to 0444 or 0555 - (i.e., read-only, with execute permission enabled if the file was - originally executable). Note that possible `setuid` and `setgid` - bits are cleared. Setuid and setgid programs are not currently - supported by Nix. This is because the Nix archives used in - deployment have no concept of ownership information, and because it - makes the build result dependent on the user performing the build. diff --git a/doc/manual/source/language/evaluation.md b/doc/manual/source/language/evaluation.md new file mode 100644 index 00000000000..980942c92b9 --- /dev/null +++ b/doc/manual/source/language/evaluation.md @@ -0,0 +1,77 @@ +# Evaluation + +Evaluation is the process of turning a Nix expression into a [Nix value](types.md). + +This happens by a number of rules, such as: +- Constructing values from literals. + For example the number literal `1` is turned into the number value `1`. +- Applying operators + For example the addition operator `+` is applied to two number values to produce a new number value. +- Applying built-in functions + For example the expression `builtins.isInt 1` is evaluated to `true`. +- Applying user-defined functions + For example the expression `(x: x + 1) 10` can[*](#laziness) be thought of rewriting `x` in the function body to the argument, `10 + 1`, which is then evaluated to `11`. + +These rules are applied as needed, driven by the specific use of the expression. For example, this can occur in the Nix command line interface or interactively with the [repl (read-eval-print loop)](@docroot@/command-ref/new-cli/nix3-repl.md), which is a useful tool when learning about evaluation. + +# Details + +## Values {#values} + +Nix values can be thought of as a subset of Nix expressions. +For example, the expression `1 + 2` is not a value, because it can be reduced to `3`. The expression `3` is a value, because it cannot be reduced any further. + +Evaluation normally happens by applying rules to the "head" of the expression, which is the outermost part of the expression. The head of an expression like `[ 1 2 ]` is the list literal (`[ a1 a2 ]`), for `1 + 2` it is the addition operator (`+`), and for `f 1` it is the function application "operator" (` `). + +After applying all possible rules to the head until no rules can be applied, the expression is in "weak head normal form" (WHNF). This means that the outermost constructor of the expression is evaluated, but the inner values may or may not be. "Weak" only signifies that the expression may be a function. This is an historical or academic artifact, and Nix has no use for the non-weak "head normal form". + +## Laziness and thunks {#laziness} + +The Nix language implements _call by need_ (as opposed to _call by value_ or _call by reference_). Call by need is commonly known as laziness in functional programming, as it is a specific implementation of the concept where evaluation is deferred until the result is required, aiming to only evaluate the parts of an expression that are needed to produce the final result. + +Furthermore, the result of evaluation is preserved, in values, in `let` bindings, in function _parameters_, which behave a lot like `let` bindings, but with the notable exception of function _calls_. Results of function calls rely on being put into `let` bindings, etc to be reused. + +When discussing the process of evaluation in lower level terms, we may define values not as a subset of expressions, but separately, where each "value" is either a data constructor, a function or a _thunk_. A thunk is a delayed computation, represented by an expression reference and a "closure" – the values for the lexical scope around the delayed expression. + +As a user of the language, you generally don't have to think about thunks, as they are not part of the language semantics, but you may encounter them in the repl, in the [C API] or in discussions. + +## Strictness + +Instead of thinking about thunks, it is often more productive to think in terms of _strictness_. +This term is used in functional programming to refer to the opposite of laziness, i.e. not just for something like error propagation. It refers to the need to evaluate certain expressions before evaluation can produce any result. + +Statements about strictness usually implicitly refer to weak head normal form. +For example, we can say that the following function is strict in its argument: + +```nix +x: isAttrs x || isFunction x +``` + +The above function must be strict in its argument `x` because determining its type requires evaluating `x` to at least some degree. + +The following function is not strict in its argument: + +```nix +x: { isOk = isAttrs x || isFunction x; } +``` + +It is not strict, because it can return the attribute set before evaluating `x`. +The attribute value for `isOk` _is_ strict in `x`. + +A function with a _set pattern_ is always strict in its argument, as a consequence of checking the argument's type and/or attribute names: + +```nix +let f = { ... }: "ok"; +in f (throw "kablam") +=> error: kablam +``` + +However, a set pattern does not add any strictness beyond WHNF of the attribute set argument. + +```nix +let f = orig@{ x, ... }: "ok"; +in f { x = throw "error"; y = throw "error"; } +=> "ok" +``` + +[C API]: @docroot@/c-api.md diff --git a/doc/manual/source/language/import-from-derivation.md b/doc/manual/source/language/import-from-derivation.md index e901f5bcf5b..f161c6fe391 100644 --- a/doc/manual/source/language/import-from-derivation.md +++ b/doc/manual/source/language/import-from-derivation.md @@ -71,8 +71,9 @@ Boxes are data structures, arrow labels are transformations. | evaluate | | | | | | | | | V | | | -| .------------. | | .------------------. | -| | derivation |----|-instantiate-|->| store derivation | | +| .------------. | | | +| | derivation | | | .------------------. | +| | expression |----|-instantiate-|->| store derivation | | | '------------' | | '------------------' | | | | | | | | | realise | diff --git a/doc/manual/source/language/index.md b/doc/manual/source/language/index.md index 2bfdbb8a0c6..1eb14e96d36 100644 --- a/doc/manual/source/language/index.md +++ b/doc/manual/source/language/index.md @@ -1,6 +1,6 @@ # Nix Language -The Nix language is designed for conveniently creating and composing *derivations* – precise descriptions of how contents of existing files are used to derive new files. +The Nix language is designed for conveniently creating and composing [derivations](@docroot@/glossary.md#gloss-derivation) – precise descriptions of how contents of existing files are used to derive new files. > **Tip** > @@ -11,7 +11,14 @@ The language is: - *domain-specific* - It comes with [built-in functions](@docroot@/language/builtins.md) to integrate with the Nix store, which manages files and performs the derivations declared in the Nix language. + The Nix language is purpose-built for working with text files. + Its most characteristic features are: + + - [File system path primitives](@docroot@/language/types.md#type-path), for accessing source files + - [Indented strings](@docroot@/language/string-literals.md) and [string interpolation](@docroot@/language/string-interpolation.md), for creating file contents + - [Strings with contexts](@docroot@/language/string-context.md), for transparently linking files + + It comes with [built-in functions](@docroot@/language/builtins.md) to integrate with the [Nix store](@docroot@/store/index.md), which manages files and enables [realising](@docroot@/glossary.md#gloss-realise) derivations declared in the Nix language. - *declarative* diff --git a/doc/manual/source/language/operators.md b/doc/manual/source/language/operators.md index dbf2441cb7c..ab74e8a9999 100644 --- a/doc/manual/source/language/operators.md +++ b/doc/manual/source/language/operators.md @@ -196,7 +196,7 @@ All comparison operators are implemented in terms of `<`, and the following equi ## Logical implication -Equivalent to `!`*b1* `||` *b2*. +Equivalent to `!`*b1* `||` *b2* (or `if` *b1* `then` *b2* `else true`) [Logical implication]: #logical-implication diff --git a/doc/manual/source/language/string-context.md b/doc/manual/source/language/string-context.md index 6a3482cfd95..0d8fcdefa91 100644 --- a/doc/manual/source/language/string-context.md +++ b/doc/manual/source/language/string-context.md @@ -13,8 +13,8 @@ The purpose of string contexts is to collect non-string values attached to strin [string concatenation](./operators.md#string-concatenation), [string interpolation](./string-interpolation.md), and similar operations. -The idea is that a user can combine together values to create a build instructions for derivations without manually keeping track of where they come from. -Then the Nix language implicitly does that bookkeeping to efficiently obtain the closure of derivation inputs. +The idea is that a user can reference other files when creating text files through Nix expressions, without manually keeping track of the exact paths. +Nix will ensure that the all referenced files are accessible – that all [store paths](@docroot@/glossary.md#gloss-store-path) are [valid](@docroot@/glossary.md#gloss-validity). > **Note** > @@ -115,7 +115,7 @@ It creates an [attribute set] representing the string context, which can be insp ## Clearing string contexts -[`buitins.unsafeDiscardStringContext`](./builtins.md#builtins-unsafeDiscardStringContext) will make a copy of a string, but with an empty string context. +[`builtins.unsafeDiscardStringContext`](./builtins.md#builtins-unsafeDiscardStringContext) will make a copy of a string, but with an empty string context. The returned string can be used in more ways, e.g. by operators that require the string context to be empty. The requirement to explicitly discard the string context in such use cases helps ensure that string context elements are not lost by mistake. The "unsafe" marker is only there to remind that Nix normally guarantees that dependencies are tracked, whereas the returned string has lost them. diff --git a/doc/manual/source/language/string-interpolation.md b/doc/manual/source/language/string-interpolation.md index 27780dcbb39..a503d5f04bd 100644 --- a/doc/manual/source/language/string-interpolation.md +++ b/doc/manual/source/language/string-interpolation.md @@ -22,9 +22,9 @@ Rather than writing "--with-freetype2-library=" + freetype + "/lib" ``` -(where `freetype` is a [derivation]), you can instead write +(where `freetype` is a [derivation expression]), you can instead write -[derivation]: @docroot@/glossary.md#gloss-derivation +[derivation expression]: @docroot@/glossary.md#gloss-derivation-expression ```nix "--with-freetype2-library=${freetype}/lib" @@ -148,7 +148,7 @@ An expression that is interpolated must evaluate to one of the following: - `__toString` must be a function that takes the attribute set itself and returns a string - `outPath` must be a string - This includes [derivations](./derivations.md) or [flake inputs](@docroot@/command-ref/new-cli/nix3-flake.md#flake-inputs) (experimental). + This includes [derivation expressions](./derivations.md) or [flake inputs](@docroot@/command-ref/new-cli/nix3-flake.md#flake-inputs) (experimental). A string interpolates to itself. diff --git a/doc/manual/source/language/syntax.md b/doc/manual/source/language/syntax.md index 506afbea130..85162db747a 100644 --- a/doc/manual/source/language/syntax.md +++ b/doc/manual/source/language/syntax.md @@ -225,8 +225,8 @@ passed in first , e.g., ```nix let add = { __functor = self: x: x + self.x; }; - inc = add // { x = 1; }; -in inc 1 + inc = add // { x = 1; }; # inc is { x = 1; __functor = (...) } +in inc 1 # equivalent of `add.__functor add 1` i.e. `1 + self.x` ``` evaluates to `2`. This can be used to attach metadata to a function @@ -443,7 +443,7 @@ three kinds of patterns: This works on any set that contains at least the three named attributes. - It is possible to provide *default values* for attributes, in + - It is possible to provide *default values* for attributes, in which case they are allowed to be missing. A default value is specified by writing `name ? e`, where *e* is an arbitrary expression. For example, @@ -503,6 +503,45 @@ three kinds of patterns: > [ 23 {} ] > ``` + - All bindings introduced by the function are in scope in the entire function expression; not just in the body. + It can therefore be used in default values. + + > **Example** + > + > A parameter (`x`), is used in the default value for another parameter (`y`): + > + > ```nix + > let + > f = { x, y ? [x] }: { inherit y; }; + > in + > f { x = 3; } + > ``` + > + > This evaluates to: + > + > ```nix + > { + > y = [ 3 ]; + > } + > ``` + + > **Example** + > + > The binding of an `@` pattern, `args`, is used in the default value for a parameter, `x`: + > + > ```nix + > let + > f = args@{ x ? args.a, ... }: x; + > in + > f { a = 1; } + > ``` + > + > This evaluates to: + > + > ```nix + > 1 + > ``` + Note that functions do not have names. If you want to give them a name, you can bind them to an attribute, e.g., diff --git a/doc/manual/source/package-management/garbage-collector-roots.md b/doc/manual/source/package-management/garbage-collector-roots.md index 30c5b7f8ddc..925a3316239 100644 --- a/doc/manual/source/package-management/garbage-collector-roots.md +++ b/doc/manual/source/package-management/garbage-collector-roots.md @@ -12,7 +12,7 @@ $ ln -s /nix/store/d718ef...-foo /nix/var/nix/gcroots/bar That is, after this command, the garbage collector will not remove `/nix/store/d718ef...-foo` or any of its dependencies. -Subdirectories of `prefix/nix/var/nix/gcroots` are also searched for -symlinks. Symlinks to non-store paths are followed and searched for -roots, but symlinks to non-store paths *inside* the paths reached in -that way are not followed to prevent infinite recursion. +Subdirectories of `prefix/nix/var/nix/gcroots` are searched +recursively. Symlinks to store paths count as roots. Symlinks to +non-store paths are ignored, unless the non-store path is itself a +symlink to a store path. \ No newline at end of file diff --git a/doc/manual/source/protocols/derivation-aterm.md b/doc/manual/source/protocols/derivation-aterm.md index 1ba757ae024..99e3c2be630 100644 --- a/doc/manual/source/protocols/derivation-aterm.md +++ b/doc/manual/source/protocols/derivation-aterm.md @@ -1,6 +1,8 @@ # Derivation "ATerm" file format -For historical reasons, [derivations](@docroot@/glossary.md#gloss-store-derivation) are stored on-disk in [ATerm](https://homepages.cwi.nl/~daybuild/daily-books/technology/aterm-guide/aterm-guide.html) format. +For historical reasons, [store derivations][store derivation] are stored on-disk in [ATerm](https://homepages.cwi.nl/~daybuild/daily-books/technology/aterm-guide/aterm-guide.html) format. + +## The ATerm format used Derivations are serialised in one of the following formats: @@ -17,3 +19,20 @@ Derivations are serialised in one of the following formats: The only `version-string`s that are in use today are for [experimental features](@docroot@/development/experimental-features.md): - `"xp-dyn-drv"` for the [`dynamic-derivations`](@docroot@/development/experimental-features.md#xp-feature-dynamic-derivations) experimental feature. + +## Use for encoding to store object + +When derivation is encoded to a [store object] we make the following choices: + +- The store path name is the derivation name with `.drv` suffixed at the end + + Indeed, the ATerm format above does *not* contain the name of the derivation, on the assumption that a store path will also be provided out-of-band. + +- The derivation is content-addressed using the ["Text" method] of content-addressing derivations + +Currently we always encode derivations to store object using the ATerm format (and the previous two choices), +but we reserve the option to encode new sorts of derivations differently in the future. + +[store derivation]: @docroot@/glossary.md#gloss-store-derivation +[store object]: @docroot@/glossary.md#gloss-store-object +["Text" method]: @docroot@/store/store-object/content-address.md#method-text diff --git a/doc/manual/source/protocols/json/derivation.md b/doc/manual/source/protocols/json/derivation.md index 2f85340d6c5..04881776abc 100644 --- a/doc/manual/source/protocols/json/derivation.md +++ b/doc/manual/source/protocols/json/derivation.md @@ -24,7 +24,7 @@ is a JSON object with the following fields: * `method`: - For an output which will be [content addresed], a string representing the [method](@docroot@/store/store-object/content-address.md) of content addressing that is chosen. + For an output which will be [content addressed], a string representing the [method](@docroot@/store/store-object/content-address.md) of content addressing that is chosen. Valid method strings are: - [`flat`](@docroot@/store/store-object/content-address.md#method-flat) @@ -35,9 +35,10 @@ is a JSON object with the following fields: Otherwise, `null`. * `hashAlgo`: - For an output which will be [content addresed], the name of the hash algorithm used. + For an output which will be [content addressed], the name of the hash algorithm used. Valid algorithm strings are: + - `blake3` - `md5` - `sha1` - `sha256` @@ -90,3 +91,7 @@ is a JSON object with the following fields: * `env`: The environment passed to the `builder`. + +* `structuredAttrs`: + [Strucutured Attributes](@docroot@/store/derivation/index.md#structured-attrs), only defined if the derivation contains them. + Structured attributes are JSON, and thus embedded as-is. diff --git a/doc/manual/source/protocols/json/store-object-info.md b/doc/manual/source/protocols/json/store-object-info.md index 6b4f4843711..b7348538c35 100644 --- a/doc/manual/source/protocols/json/store-object-info.md +++ b/doc/manual/source/protocols/json/store-object-info.md @@ -41,10 +41,10 @@ In other words, the same store object residing in different store could have dif * `deriver`: - If known, the path to the [derivation] from which this store object was produced. + If known, the path to the [store derivation] from which this store object was produced. Otherwise `null`. - [derivation]: @docroot@/glossary.md#gloss-store-derivation + [store derivation]: @docroot@/glossary.md#gloss-store-derivation * `registrationTime` (optional): diff --git a/doc/manual/source/protocols/store-path.md b/doc/manual/source/protocols/store-path.md index 8ec6f8201ff..5be2355015f 100644 --- a/doc/manual/source/protocols/store-path.md +++ b/doc/manual/source/protocols/store-path.md @@ -7,7 +7,7 @@ The format of this specification is close to [Extended Backus–Naur form](https Regular users do *not* need to know this information --- store paths can be treated as black boxes computed from the properties of the store objects they refer to. But for those interested in exactly how Nix works, e.g. if they are reimplementing it, this information can be useful. -[store path](@docroot@/store/store-path.md) +[store path]: @docroot@/store/store-path.md ## Store path proper @@ -20,14 +20,17 @@ where - `store-dir` = the [store directory](@docroot@/store/store-path.md#store-directory) -- `digest` = base-32 representation of the first 160 bits of a [SHA-256] hash of `fingerprint` +- `digest` = base-32 representation of the compressed to 160 bits [SHA-256] hash of `fingerprint` - This the hash part of the store name +For the definition of the hash compression algorithm, please refer to the section 5.1 of +the [Nix thesis](https://edolstra.github.io/pubs/phd-thesis.pdf), which also defines the +specifics of base-32 encoding. Note that base-32 encoding processes the hash bytestring from +the end, while base-16 processes in from the beginning. ## Fingerprint - ```ebnf - fingerprint = type ":" sha256 ":" inner-digest ":" store ":" name + fingerprint = type ":sha256:" inner-digest ":" store ":" name ``` Note that it includes the location of the store as well as the name to make sure that changes to either of those are reflected in the hash @@ -53,7 +56,7 @@ where method of content addressing store objects, if the hash algorithm is [SHA-256]. Just like in the "Text" case, we can have the store objects referenced by their paths. - Additionally, we can have an optional `:self` label to denote self reference. + Additionally, we can have an optional `:self` label to denote self-reference. - ```ebnf | "output:" id @@ -70,7 +73,8 @@ where `id` is the name of the output (usually, "out"). For content-addressed store objects, `id`, is always "out". -- `inner-digest` = base-16 representation of a SHA-256 hash of `inner-fingerprint` +- `inner-digest` = base-16 representation of a SHA-256 hash of `inner-fingerprint`. + The base-16 encoding uses lower-cased hex digits. ## Inner fingerprint @@ -82,7 +86,7 @@ where - if `type` = `"source:" ...`: - the hash of the [Nix Archive (NAR)] serialization of the [file system object](@docroot@/store/file-system-object.md) of the store object. + the [Nix Archive (NAR)] serialization of the [file system object](@docroot@/store/file-system-object.md) of the store object. - if `type` = `"output:" id`: diff --git a/doc/manual/source/protocols/tarball-fetcher.md b/doc/manual/source/protocols/tarball-fetcher.md index 5cff05d66ce..e8cda784cc9 100644 --- a/doc/manual/source/protocols/tarball-fetcher.md +++ b/doc/manual/source/protocols/tarball-fetcher.md @@ -46,7 +46,7 @@ defined as the timestamp of the newest file inside the tarball. This protocol is supported by Gitea since v1.22.1 and by Forgejo since v7.0.4/v8.0.0 and can be used with the following flake URL schema: ``` -https://///archive/.tar.gz +https://///archive/.tar.gz ``` > **Example** diff --git a/doc/manual/source/release-notes/rl-0.8.md b/doc/manual/source/release-notes/rl-0.8.md index 626c0c92b79..5ba6e0e7217 100644 --- a/doc/manual/source/release-notes/rl-0.8.md +++ b/doc/manual/source/release-notes/rl-0.8.md @@ -39,29 +39,29 @@ Nix 0.8 has the following improvements: notion of “closure store expressions†is gone (and so is the notion of “successorsâ€); the file system references of a store path are now just stored in the database. - + For instance, given any store path, you can query its closure: - + $ nix-store -qR $(which firefox) ... lots of paths ... - + Also, Nix now remembers for each store path the derivation that built it (the “deriverâ€): - + $ nix-store -qR $(which firefox) /nix/store/4b0jx7vq80l9aqcnkszxhymsf1ffa5jd-firefox-1.0.1.drv - + So to see the build-time dependencies, you can do - + $ nix-store -qR $(nix-store -qd $(which firefox)) - + or, in a nicer format: - + $ nix-store -q --tree $(nix-store -qd $(which firefox)) - + File system references are also stored in reverse. For instance, you can query all paths that directly or indirectly use a certain Glibc: - + $ nix-store -q --referrers-closure \ /nix/store/8lz9yc6zgmc0vlqmn2ipcpkjlmbi51vv-glibc-2.3.4 @@ -92,28 +92,28 @@ Nix 0.8 has the following improvements: - `nix-channel` has new operations `--list` and `--remove`. - New ways of installing components into user environments: - + - Copy from another user environment: - + $ nix-env -i --from-profile .../other-profile firefox - + - Install a store derivation directly (bypassing the Nix expression language entirely): - + $ nix-env -i /nix/store/z58v41v21xd3...-aterm-2.3.1.drv - + (This is used to implement `nix-install-package`, which is therefore immune to evolution in the Nix expression language.) - + - Install an already built store path directly: - + $ nix-env -i /nix/store/hsyj5pbn0d9i...-aterm-2.3.1 - + - Install the result of a Nix expression specified as a command-line argument: - + $ nix-env -f .../i686-linux.nix -i -E 'x: x.firefoxWrapper' - + The difference with the normal installation mode is that `-E` does not use the `name` attributes of derivations. Therefore, this can be used to disambiguate multiple derivations with the @@ -127,7 +127,7 @@ Nix 0.8 has the following improvements: - Implemented a concurrent garbage collector. It is now always safe to run the garbage collector, even if other Nix operations are happening simultaneously. - + However, there can still be GC races if you use `nix-instantiate` and `nix-store --realise` directly to build things. To prevent races, use the @@ -147,13 +147,13 @@ Nix 0.8 has the following improvements: - The behaviour of the garbage collector can be changed globally by setting options in `/nix/etc/nix/nix.conf`. - + - `gc-keep-derivations` specifies whether deriver links should be followed when searching for live paths. - + - `gc-keep-outputs` specifies whether outputs of derivations should be followed when searching for live paths. - + - `env-keep-derivations` specifies whether user environments should store the paths of derivations when they are added (thus keeping the derivations alive). diff --git a/doc/manual/source/release-notes/rl-2.0.md b/doc/manual/source/release-notes/rl-2.0.md index 9f6d4aa8323..aad0de21189 100644 --- a/doc/manual/source/release-notes/rl-2.0.md +++ b/doc/manual/source/release-notes/rl-2.0.md @@ -8,13 +8,13 @@ The following incompatible changes have been made: It has been superseded by the binary cache substituter mechanism since several years. As a result, the following programs have been removed: - + - `nix-pull` - + - `nix-generate-patches` - + - `bsdiff` - + - `bspatch` - The “copy from other stores†substituter mechanism @@ -58,26 +58,26 @@ This release has the following new features: `nix-build`, `nix-shell -p`, `nix-env -qa`, `nix-instantiate --eval`, `nix-push` and `nix-copy-closure`. It has the following major features: - + - Unlike the legacy commands, it has a consistent way to refer to packages and package-like arguments (like store paths). For example, the following commands all copy the GNU Hello package to a remote machine: - + nix copy --to ssh://machine nixpkgs.hello - + nix copy --to ssh://machine /nix/store/0i2jd68mp5g6h2sa5k9c85rb80sn8hi9-hello-2.10 - + nix copy --to ssh://machine '(with import {}; hello)' - + By contrast, `nix-copy-closure` only accepted store paths as arguments. - + - It is self-documenting: `--help` shows all available command-line arguments. If `--help` is given after a subcommand, it shows examples for that subcommand. `nix --help-config` shows all configuration options. - + - It is much less verbose. By default, it displays a single-line progress indicator that shows how many packages are left to be built or downloaded, and (if there are running builds) the most @@ -85,7 +85,7 @@ This release has the following new features: last few lines of builder output. The full build log can be retrieved using `nix log`. - + - It [provides](https://github.com/NixOS/nix/commit/b8283773bd64d7da6859ed520ee19867742a03ba) all `nix.conf` configuration options as command line flags. For @@ -93,122 +93,122 @@ This release has the following new features: http-connections 100` you can write `--http-connections 100`. Boolean options can be written as `--foo` or `--no-foo` (e.g. `--no-auto-optimise-store`). - + - Many subcommands have a `--json` flag to write results to stdout in JSON format. - + > **Warning** - > + > > Please note that the `nix` command is a work in progress and the > interface is subject to change. - + It provides the following high-level (“porcelainâ€) subcommands: - + - `nix build` is a replacement for `nix-build`. - + - `nix run` executes a command in an environment in which the specified packages are available. It is (roughly) a replacement for `nix-shell -p`. Unlike that command, it does not execute the command in a shell, and has a flag (`-c`) that specifies the unquoted command line to be executed. - + It is particularly useful in conjunction with chroot stores, allowing Linux users who do not have permission to install Nix in `/nix/store` to still use binary substitutes that assume `/nix/store`. For example, - + nix run --store ~/my-nix nixpkgs.hello -c hello --greeting 'Hi everybody!' - + downloads (or if not substitutes are available, builds) the GNU Hello package into `~/my-nix/nix/store`, then runs `hello` in a mount namespace where `~/my-nix/nix/store` is mounted onto `/nix/store`. - + - `nix search` replaces `nix-env -qa`. It searches the available packages for occurrences of a search string in the attribute name, package name or description. Unlike `nix-env -qa`, it has a cache to speed up subsequent searches. - + - `nix copy` copies paths between arbitrary Nix stores, generalising `nix-copy-closure` and `nix-push`. - + - `nix repl` replaces the external program `nix-repl`. It provides an interactive environment for evaluating and building Nix expressions. Note that it uses `linenoise-ng` instead of GNU Readline. - + - `nix upgrade-nix` upgrades Nix to the latest stable version. This requires that Nix is installed in a profile. (Thus it won’t work on NixOS, or if it’s installed outside of the Nix store.) - + - `nix verify` checks whether store paths are unmodified and/or “trusted†(see below). It replaces `nix-store --verify` and `nix-store --verify-path`. - + - `nix log` shows the build log of a package or path. If the build log is not available locally, it will try to obtain it from the configured substituters (such as [cache.nixos.org](https://cache.nixos.org/), which now provides build logs). - + - `nix edit` opens the source code of a package in your editor. - + - `nix eval` replaces `nix-instantiate --eval`. - + - `nix why-depends` shows why one store path has another in its closure. This is primarily useful to finding the causes of closure bloat. For example, - + nix why-depends nixpkgs.vlc nixpkgs.libdrm.dev - + shows a chain of files and fragments of file contents that cause the VLC package to have the “dev†output of `libdrm` in its closure — an undesirable situation. - + - `nix path-info` shows information about store paths, replacing `nix-store -q`. A useful feature is the option `--closure-size` (`-S`). For example, the following command show the closure sizes of every path in the current NixOS system closure, sorted by size: - + nix path-info -rS /run/current-system | sort -nk2 - + - `nix optimise-store` replaces `nix-store --optimise`. The main difference is that it has a progress indicator. - + A number of low-level (“plumbingâ€) commands are also available: - + - `nix ls-store` and `nix ls-nar` list the contents of a store path or NAR file. The former is primarily useful in conjunction with remote stores, e.g. - + nix ls-store --store https://cache.nixos.org/ -lR /nix/store/0i2jd68mp5g6h2sa5k9c85rb80sn8hi9-hello-2.10 - + lists the contents of path in a binary cache. - + - `nix cat-store` and `nix cat-nar` allow extracting a file from a store path or NAR file. - + - `nix dump-path` writes the contents of a store path to stdout in NAR format. This replaces `nix-store --dump`. - + - `nix show-derivation` displays a store derivation in JSON format. This is an alternative to `pp-aterm`. - + - `nix add-to-store` replaces `nix-store --add`. - + - `nix sign-paths` signs store paths. - + - `nix copy-sigs` copies signatures from one store to another. - + - `nix show-config` shows all configuration options and their current values. @@ -224,11 +224,11 @@ This release has the following new features: `nix-copy-closure`, `nix-push` and substitution are all instances of the general notion of copying paths between different kinds of Nix stores. - + Stores are specified using an URI-like syntax, e.g. or . The following store types are supported: - + - `LocalStore` (stori URI `local` or an absolute path) and the misnamed `RemoteStore` (`daemon`) provide access to a local Nix store, the latter via the Nix daemon. You can use `auto` or the @@ -236,63 +236,63 @@ This release has the following new features: whether you have write permission to the Nix store. It is no longer necessary to set the `NIX_REMOTE` environment variable to use the Nix daemon. - + As noted above, `LocalStore` now supports chroot builds, allowing the “physical†location of the Nix store (e.g. `/home/alice/nix/store`) to differ from its “logical†location (typically `/nix/store`). This allows non-root users to use Nix while still getting the benefits from prebuilt binaries from [cache.nixos.org](https://cache.nixos.org/). - + - `BinaryCacheStore` is the abstract superclass of all binary cache stores. It supports writing build logs and NAR content listings in JSON format. - + - `HttpBinaryCacheStore` (`http://`, `https://`) supports binary caches via HTTP or HTTPS. If the server supports `PUT` requests, it supports uploading store paths via commands such as `nix copy`. - + - `LocalBinaryCacheStore` (`file://`) supports binary caches in the local filesystem. - + - `S3BinaryCacheStore` (`s3://`) supports binary caches stored in Amazon S3, if enabled at compile time. - + - `LegacySSHStore` (`ssh://`) is used to implement remote builds and `nix-copy-closure`. - + - `SSHStore` (`ssh-ng://`) supports arbitrary Nix operations on a remote machine via the same protocol used by `nix-daemon`. - Security has been improved in various ways: - + - Nix now stores signatures for local store paths. When paths are copied between stores (e.g., copied from a binary cache to a local store), signatures are propagated. - + Locally-built paths are signed automatically using the secret keys specified by the `secret-key-files` store option. Secret/public key pairs can be generated using `nix-store --generate-binary-cache-key`. - + In addition, locally-built store paths are marked as “ultimately trustedâ€, but this bit is not propagated when paths are copied between stores. - + - Content-addressable store paths no longer require signatures — they can be imported into a store by unprivileged users even if they lack signatures. - + - The command `nix verify` checks whether the specified paths are trusted, i.e., have a certain number of trusted signatures, are ultimately trusted, or are content-addressed. - + - Substitutions from binary caches [now](https://github.com/NixOS/nix/commit/ecbc3fedd3d5bdc5a0e1a0a51b29062f2874ac8b) require signatures by default. This was already the case on NixOS. - + - In Linux sandbox builds, we [now](https://github.com/NixOS/nix/commit/eba840c8a13b465ace90172ff76a0db2899ab11b) use `/build` instead of `/tmp` as the temporary build directory. @@ -309,7 +309,7 @@ This release has the following new features: hash or commit hash is specified. For example, calls to `builtins.fetchGit` are only allowed if a `rev` attribute is specified. - + The goal of this feature is to enable true reproducibility and traceability of builds (including NixOS system configurations) at the evaluation level. For example, in the future, `nixos-rebuild` @@ -367,21 +367,21 @@ This release has the following new features: log will be shown if a build fails. - Networking has been improved: - + - HTTP/2 is now supported. This makes binary cache lookups [much more efficient](https://github.com/NixOS/nix/commit/90ad02bf626b885a5dd8967894e2eafc953bdf92). - + - We now retry downloads on many HTTP errors, making binary caches substituters more resilient to temporary failures. - + - HTTP credentials can now be configured via the standard `netrc` mechanism. - + - If S3 support is enabled at compile time, URIs are [supported](https://github.com/NixOS/nix/commit/9ff9c3f2f80ba4108e9c945bbfda2c64735f987b) in all places where Nix allows URIs. - + - Brotli compression is now supported. In particular, [cache.nixos.org](https://cache.nixos.org/) build logs are now compressed using Brotli. @@ -431,9 +431,9 @@ The Nix language has the following new features: - Derivation attributes can now reference the outputs of the derivation using the `placeholder` builtin function. For example, the attribute - + configureFlags = "--prefix=${placeholder "out"} --includedir=${placeholder "dev"}"; - + will cause the `configureFlags` environment variable to contain the actual store paths corresponding to the `out` and `dev` outputs. @@ -444,7 +444,7 @@ The following builtin functions are new or extended: Nixpkgs, which fetches at build time and cannot be used to fetch Nix expressions during evaluation. A typical use case is to import external NixOS modules from your configuration, e.g. - + imports = [ (builtins.fetchGit https://github.com/edolstra/dwarffs + "/module.nix") ]; - Similarly, `builtins.fetchMercurial` allows you to fetch Mercurial @@ -485,7 +485,7 @@ The Nix build environment has the following changes: builder via the file `.attrs.json` in the builder’s temporary directory. This obviates the need for `passAsFile` since JSON files have no size restrictions, unlike process environments. - + [As a convenience to Bash builders](https://github.com/NixOS/nix/commit/2d5b1b24bf70a498e4c0b378704cfdb6471cc699), Nix writes a script named `.attrs.sh` to the builder’s directory diff --git a/doc/manual/source/release-notes/rl-2.19.md b/doc/manual/source/release-notes/rl-2.19.md index e6a93c7eaae..06c704324dd 100644 --- a/doc/manual/source/release-notes/rl-2.19.md +++ b/doc/manual/source/release-notes/rl-2.19.md @@ -31,7 +31,7 @@ - To operate on a flake outside the current directory, you must now pass `--flake path/to/flake`. - The flake-specific flags `--recreate-lock-file` and `--update-input` have been removed from all commands operating on installables. - They are superceded by `nix flake update`. + They are superseded by `nix flake update`. - Commit signature verification for the [`builtins.fetchGit`](@docroot@/language/builtins.md#builtins-fetchGit) is added as the new [`verified-fetches` experimental feature](@docroot@/development/experimental-features.md#xp-feature-verified-fetches). diff --git a/doc/manual/source/release-notes/rl-2.23.md b/doc/manual/source/release-notes/rl-2.23.md index 249c183e66d..e6b0e9ffcec 100644 --- a/doc/manual/source/release-notes/rl-2.23.md +++ b/doc/manual/source/release-notes/rl-2.23.md @@ -15,7 +15,7 @@ - Modify `nix derivation {add,show}` JSON format [#9866](https://github.com/NixOS/nix/issues/9866) [#10722](https://github.com/NixOS/nix/pull/10722) The JSON format for derivations has been slightly revised to better conform to our [JSON guidelines](@docroot@/development/cli-guideline.md#returning-future-proof-json). - In particular, the hash algorithm and content addressing method of content-addresed derivation outputs are now separated into two fields `hashAlgo` and `method`, + In particular, the hash algorithm and content addressing method of content-addressed derivation outputs are now separated into two fields `hashAlgo` and `method`, rather than one field with an arcane `:`-separated format. This JSON format is only used by the experimental `nix derivation` family of commands, at this time. diff --git a/doc/manual/source/release-notes/rl-2.24.md b/doc/manual/source/release-notes/rl-2.24.md index 08ec65be902..d4af3cb5174 100644 --- a/doc/manual/source/release-notes/rl-2.24.md +++ b/doc/manual/source/release-notes/rl-2.24.md @@ -173,7 +173,7 @@ **Deprecation**: Use `nix32` instead of `base32` as `toHashFormat` For the builtin `convertHash`, the `toHashFormat` parameter now accepts the same hash formats as the `--to`/`--from` - parameters of the `nix hash conert` command: `"base16"`, `"nix32"`, `"base64"`, and `"sri"`. The former `"base32"` value + parameters of the `nix hash convert` command: `"base16"`, `"nix32"`, `"base64"`, and `"sri"`. The former `"base32"` value remains as a deprecated alias for `"nix32"`. Please convert your code from: ```nix @@ -269,7 +269,7 @@ e.g. `--warn-large-path-threshold 100M`. -# Contributors +## Contributors This release was made possible by the following 43 contributors: diff --git a/doc/manual/source/release-notes/rl-2.25.md b/doc/manual/source/release-notes/rl-2.25.md index 29e3e509cf0..cfde8b1ef8f 100644 --- a/doc/manual/source/release-notes/rl-2.25.md +++ b/doc/manual/source/release-notes/rl-2.25.md @@ -77,7 +77,7 @@ `` is also known as the builtin derivation builder `builtin:fetchurl`. It's not to be confused with the evaluation-time function `builtins.fetchurl`, which was not affected by this issue. -# Contributors +## Contributors This release was made possible by the following 58 contributors: diff --git a/doc/manual/source/release-notes/rl-2.26.md b/doc/manual/source/release-notes/rl-2.26.md new file mode 100644 index 00000000000..0c3df828f79 --- /dev/null +++ b/doc/manual/source/release-notes/rl-2.26.md @@ -0,0 +1,128 @@ +# Release 2.26.0 (2025-01-22) + +- Support for relative path inputs [#10089](https://github.com/NixOS/nix/pull/10089) + + Flakes can now refer to other flakes in the same repository using relative paths, e.g. + ```nix + inputs.foo.url = "path:./foo"; + ``` + uses the flake in the `foo` subdirectory of the referring flake. For more information, see the documentation on [the `path` flake input type](@docroot@/command-ref/new-cli/nix3-flake.md#path-fetcher). + + This feature required a change to the lock file format. Previous Nix versions will not be able to use lock files that have locks for relative path inputs in them. + +- Flake lock file generation now ignores local registries [#12019](https://github.com/NixOS/nix/pull/12019) + + When resolving indirect flake references like `nixpkgs` in `flake.nix` files, Nix will no longer use the system and user flake registries. It will only use the global flake registry and overrides given on the command line via `--override-flake`. + + This avoids accidents where users have local registry overrides that map `nixpkgs` to a `path:` flake in the local file system, which then end up in committed lock files pushed to other users. + + In the future, we may remove the use of the registry during lock file generation altogether. It's better to explicitly specify the URL of a flake input. For example, instead of + ```nix + { + outputs = { self, nixpkgs }: { ... }; + } + ``` + write + ```nix + { + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; + outputs = { self, nixpkgs }: { ... }; + } + ``` + +- `nix copy` supports `--profile` and `--out-link` [#11657](https://github.com/NixOS/nix/pull/11657) + + The `nix copy` command now has flags `--profile` and `--out-link`, similar to `nix build`. `--profile` makes a profile point to the + top-level store path, while `--out-link` create symlinks to the top-level store paths. + + For example, when updating the local NixOS system profile from a NixOS system closure on a remote machine, instead of + ``` + # nix copy --from ssh://server $path + # nix build --profile /nix/var/nix/profiles/system $path + ``` + you can now do + ``` + # nix copy --from ssh://server --profile /nix/var/nix/profiles/system $path + ``` + The advantage is that this avoids a time window where *path* is not a garbage collector root, and so could be deleted by a concurrent `nix store gc` process. + +- `nix-instantiate --eval` now supports `--raw` [#12119](https://github.com/NixOS/nix/pull/12119) + + The `nix-instantiate --eval` command now supports a `--raw` flag, when used + the evaluation result must be a string, which is printed verbatim without + quotation marks or escaping. + +- Improved `NIX_SSHOPTS` parsing for better SSH option handling [#5181](https://github.com/NixOS/nix/issues/5181) [#12020](https://github.com/NixOS/nix/pull/12020) + + The parsing of the `NIX_SSHOPTS` environment variable has been improved to handle spaces and quotes correctly. + Previously, incorrectly split SSH options could cause failures in commands like `nix-copy-closure`, + especially when using complex SSH invocations such as `-o ProxyCommand="ssh -W %h:%p ..."`. + + This change introduces a `shellSplitString` function to ensure + that `NIX_SSHOPTS` is parsed in a manner consistent with shell + behavior, addressing common parsing errors. + + For example, the following now works as expected: + + ```bash + export NIX_SSHOPTS='-o ProxyCommand="ssh -W %h:%p ..."' + ``` + + This update improves the reliability of SSH-related operations using `NIX_SSHOPTS` across Nix CLIs. + +- Nix is now built using Meson + + As proposed in [RFC 132](https://github.com/NixOS/rfcs/pull/132), Nix's build system now uses Meson/Ninja. The old Make-based build system has been removed. + +- Evaluation caching now works for dirty Git workdirs [#11992](https://github.com/NixOS/nix/pull/11992) + +## Contributors + +This release was made possible by the following 45 contributors: + +- Anatoli Babenia [**(@abitrolly)**](https://github.com/abitrolly) +- Domagoj MiÅ¡ković [**(@allrealmsoflife)**](https://github.com/allrealmsoflife) +- Yaroslav Bolyukin [**(@CertainLach)**](https://github.com/CertainLach) +- bryango [**(@bryango)**](https://github.com/bryango) +- tomberek [**(@tomberek)**](https://github.com/tomberek) +- Matej Urbas [**(@mupdt)**](https://github.com/mupdt) +- elikoga [**(@elikoga)**](https://github.com/elikoga) +- wh0 [**(@wh0)**](https://github.com/wh0) +- Félix [**(@picnoir)**](https://github.com/picnoir) +- Valentin Gagarin [**(@fricklerhandwerk)**](https://github.com/fricklerhandwerk) +- Gavin John [**(@Pandapip1)**](https://github.com/Pandapip1) +- Travis A. Everett [**(@abathur)**](https://github.com/abathur) +- Vladimir Panteleev [**(@CyberShadow)**](https://github.com/CyberShadow) +- Ilja [**(@suruaku)**](https://github.com/suruaku) +- Jason Yundt [**(@Jayman2000)**](https://github.com/Jayman2000) +- Mike Kusold [**(@kusold)**](https://github.com/kusold) +- Andy Hamon [**(@andrewhamon)**](https://github.com/andrewhamon) +- Brian McKenna [**(@puffnfresh)**](https://github.com/puffnfresh) +- Greg Curtis [**(@gcurtis)**](https://github.com/gcurtis) +- Andrew Poelstra [**(@apoelstra)**](https://github.com/apoelstra) +- Linus Heckemann [**(@lheckemann)**](https://github.com/lheckemann) +- Tristan Ross [**(@RossComputerGuy)**](https://github.com/RossComputerGuy) +- Dominique Martinet [**(@martinetd)**](https://github.com/martinetd) +- h0nIg [**(@h0nIg)**](https://github.com/h0nIg) +- Eelco Dolstra [**(@edolstra)**](https://github.com/edolstra) +- Shahar "Dawn" Or [**(@mightyiam)**](https://github.com/mightyiam) +- NAHO [**(@trueNAHO)**](https://github.com/trueNAHO) +- Ryan Hendrickson [**(@rhendric)**](https://github.com/rhendric) +- the-sun-will-rise-tomorrow [**(@the-sun-will-rise-tomorrow)**](https://github.com/the-sun-will-rise-tomorrow) +- Connor Baker [**(@ConnorBaker)**](https://github.com/ConnorBaker) +- Cole Helbling [**(@cole-h)**](https://github.com/cole-h) +- Jack Wilsdon [**(@jackwilsdon)**](https://github.com/jackwilsdon) +- ‮rekcäH nitraM‮ [**(@dwt)**](https://github.com/dwt) +- Martin Fischer [**(@not-my-profile)**](https://github.com/not-my-profile) +- John Ericson [**(@Ericson2314)**](https://github.com/Ericson2314) +- Graham Christensen [**(@grahamc)**](https://github.com/grahamc) +- Sergei Zimmerman [**(@xokdvium)**](https://github.com/xokdvium) +- Siddarth Kumar [**(@siddarthkay)**](https://github.com/siddarthkay) +- Sergei Trofimovich [**(@trofi)**](https://github.com/trofi) +- Robert Hensing [**(@roberth)**](https://github.com/roberth) +- Mutsuha Asada [**(@momeemt)**](https://github.com/momeemt) +- Parker Jones [**(@knotapun)**](https://github.com/knotapun) +- Jörg Thalheim [**(@Mic92)**](https://github.com/Mic92) +- dbdr [**(@dbdr)**](https://github.com/dbdr) +- myclevorname [**(@myclevorname)**](https://github.com/myclevorname) +- Philipp Otterbein diff --git a/doc/manual/source/release-notes/rl-2.27.md b/doc/manual/source/release-notes/rl-2.27.md new file mode 100644 index 00000000000..34da6252578 --- /dev/null +++ b/doc/manual/source/release-notes/rl-2.27.md @@ -0,0 +1,75 @@ +# Release 2.27.0 (2025-03-03) + +- `inputs.self.submodules` flake attribute [#12421](https://github.com/NixOS/nix/pull/12421) + + Flakes in Git repositories can now declare that they need Git submodules to be enabled: + ``` + { + inputs.self.submodules = true; + } + ``` + Thus, it's no longer needed for the caller of the flake to pass `submodules = true`. + +- Git LFS support [#10153](https://github.com/NixOS/nix/pull/10153) [#12468](https://github.com/NixOS/nix/pull/12468) + + The Git fetcher now supports Large File Storage (LFS). This can be enabled by passing the attribute `lfs = true` to the fetcher, e.g. + ```console + nix flake prefetch 'git+ssh://git@github.com/Apress/repo-with-large-file-storage.git?lfs=1' + ``` + + A flake can also declare that it requires LFS to be enabled: + ``` + { + inputs.self.lfs = true; + } + ``` + + Author: [**@b-camacho**](https://github.com/b-camacho), [**@kip93**](https://github.com/kip93) + +- Handle the case where a chroot store is used and some inputs are in the "host" `/nix/store` [#12512](https://github.com/NixOS/nix/pull/12512) + + The evaluator now presents a "union" filesystem view of the `/nix/store` in the host and the chroot. + + This change also removes some hacks that broke `builtins.{path,filterSource}` in chroot stores [#11503](https://github.com/NixOS/nix/issues/11503). + +- `nix flake prefetch` now has a `--out-link` option [#12443](https://github.com/NixOS/nix/pull/12443) + +- Set `FD_CLOEXEC` on sockets created by curl [#12439](https://github.com/NixOS/nix/pull/12439) + + Curl created sockets without setting `FD_CLOEXEC`/`SOCK_CLOEXEC`. This could previously cause connections to remain open forever when using commands like `nix shell`. This change sets the `FD_CLOEXEC` flag using a `CURLOPT_SOCKOPTFUNCTION` callback. + +- Add BLAKE3 hash algorithm [#12379](https://github.com/NixOS/nix/pull/12379) + + Nix now supports the BLAKE3 hash algorithm as an experimental feature (`blake3-hashes`): + + ```console + # nix hash file ./file --type blake3 --extra-experimental-features blake3-hashes + blake3-34P4p+iZXcbbyB1i4uoF7eWCGcZHjmaRn6Y7QdynLwU= + ``` + +## Contributors + +This release was made possible by the following 21 contributors: + +- Aiden Fox Ivey [**(@aidenfoxivey)**](https://github.com/aidenfoxivey) +- Ben Millwood [**(@bmillwood)**](https://github.com/bmillwood) +- Brian Camacho [**(@b-camacho)**](https://github.com/b-camacho) +- Brian McKenna [**(@puffnfresh)**](https://github.com/puffnfresh) +- Eelco Dolstra [**(@edolstra)**](https://github.com/edolstra) +- Fabian Möller [**(@B4dM4n)**](https://github.com/B4dM4n) +- Illia Bobyr [**(@ilya-bobyr)**](https://github.com/ilya-bobyr) +- Ivan Trubach [**(@tie)**](https://github.com/tie) +- John Ericson [**(@Ericson2314)**](https://github.com/Ericson2314) +- Jörg Thalheim [**(@Mic92)**](https://github.com/Mic92) +- Leandro Emmanuel Reina Kiperman [**(@kip93)**](https://github.com/kip93) +- MaxHearnden [**(@MaxHearnden)**](https://github.com/MaxHearnden) +- Philipp Otterbein +- Robert Hensing [**(@roberth)**](https://github.com/roberth) +- Sandro [**(@SuperSandro2000)**](https://github.com/SuperSandro2000) +- Sergei Zimmerman [**(@xokdvium)**](https://github.com/xokdvium) +- Silvan Mosberger [**(@infinisil)**](https://github.com/infinisil) +- Someone [**(@SomeoneSerge)**](https://github.com/SomeoneSerge) +- Steve Walker [**(@stevalkr)**](https://github.com/stevalkr) +- bcamacho2 [**(@bcamacho2)**](https://github.com/bcamacho2) +- silvanshade [**(@silvanshade)**](https://github.com/silvanshade) +- tomberek [**(@tomberek)**](https://github.com/tomberek) diff --git a/doc/manual/source/release-notes/rl-2.28.md b/doc/manual/source/release-notes/rl-2.28.md new file mode 100644 index 00000000000..93ea2cfdedf --- /dev/null +++ b/doc/manual/source/release-notes/rl-2.28.md @@ -0,0 +1,105 @@ +# Release 2.28.0 (2025-04-02) + +This is an atypical release, and for almost all intents and purposes, it is just a continuation of 2.27; not a feature release. + +We had originally set the goal of making 2.27 the Nixpkgs default for NixOS 25.05, but dependents that link to Nix need certain _interface breaking_ changes in the C++ headers. This is not something we should do in a patch release, so this is why we branched 2.28 right off 2.27 instead of `master`. + +This completes the infrastructure overhaul for the [RFC 132](https://github.com/NixOS/rfcs/blob/master/rfcs/0132-meson-builds-nix.md) switchover to meson as our build system. + +## Major changes + +- Unstable C++ API reworked + [#12836](https://github.com/NixOS/nix/pull/12836) + [#12798](https://github.com/NixOS/nix/pull/12798) + [#12773](https://github.com/NixOS/nix/pull/12773) + + Now the C++ interface confirms to common conventions much better than before: + + - All headers are expected to be included with the initial `nix/`, e.g. as `#include "nix/....hh"` (what Nix's headers now do) or `#include ` (what downstream projects may choose to do). + Likewise, the pkg-config files have `-I${includedir}` not `-I${includedir}/nix` or similar. + + Including without the `nix/` like before sometimes worked because of how for `#include` C pre-process checks the directory containing the current file, not just the lookup path, but this was not reliable. + + - All configuration headers are included explicitly by the (regular) headers that need them. + There is no more need to pass `-include` to force additional files to be included. + + - The public, installed configuration headers no longer contain implementation-specific details that are not relevant to the API. + The vast majority of definitions that were previously in there are now moved to new headers that are not installed, but used during Nix's own compilation only. + The remaining macro definitions are renamed to have `NIX_` as a prefix. + + - The name of the Nix component the header comes from + (e.g. `util`, `store`, `expr`, `flake`, etc.) + is now part of the path to the header, coming after `nix` and before the header name + (or rest of the header path, if it is already in a directory). + + Here is a contrived diff showing a few of these changes at once: + + ```diff + @@ @@ + -#include "derived-path.hh" + +#include "nix/store/derived-path.hh" + @@ @@ + +// Would include for the variables used before. But when other headers + +// need these variables. those will include these config themselves. + +#include "nix/store/config.hh" + +#include "nix/expr/config.hh" + @@ @@ + -#include "config.hh" + +// Additionally renamed to distinguish from components' config headers. + +#include "nix/util/configuration.hh" + @@ @@ + -#if HAVE_ACL_SUPPORT + +#if NIX_SUPPORT_ACL + @@ @@ + -#if HAVE_BOEHMGC + +#if NIX_USE_BOEHMGC + @@ @@ + #endif + #endif + @@ @@ + -const char *s = "hi from " SYSTEM; + +const char *s = "hi from " NIX_LOCAL_SYSTEM; + ``` + +- C API `nix_flake_init_global` removed [#5638](https://github.com/NixOS/nix/issues/5638) [#12759](https://github.com/NixOS/nix/pull/12759) + + In order to improve the modularity of the code base, we are removing a use of global state, and therefore the `nix_flake_init_global` function. + + Instead, use `nix_flake_settings_add_to_eval_state_builder`. + For example: + + ```diff + - nix_flake_init_global(ctx, settings); + - HANDLE_ERROR(ctx); + - + nix_eval_state_builder * builder = nix_eval_state_builder_new(ctx, store); + HANDLE_ERROR(ctx); + + + nix_flake_settings_add_to_eval_state_builder(ctx, settings, builder); + + HANDLE_ERROR(ctx); + ``` + + Although this change is not as critical, we figured it would be good to do this API change at the same time, also. + Also note that we try to keep the C API compatible, but we decided to break this function because it was young and likely not in widespread use yet. This frees up time to make important progress on the rest of the C API. + +## Contributors + +This earlier-than-usual release was made possible by the following 16 contributors: + +- Farid Zakaria [**(@fzakaria)**](https://github.com/fzakaria) +- Jörg Thalheim [**(@Mic92)**](https://github.com/Mic92) +- Eelco Dolstra [**(@edolstra)**](https://github.com/edolstra) +- Graham Christensen [**(@grahamc)**](https://github.com/grahamc) +- Thomas Miedema [**(@thomie)**](https://github.com/thomie) +- Brian McKenna [**(@puffnfresh)**](https://github.com/puffnfresh) +- Sergei Trofimovich [**(@trofi)**](https://github.com/trofi) +- Dmitry Bogatov [**(@KAction)**](https://github.com/KAction) +- Erik Nygren [**(@Kirens)**](https://github.com/Kirens) +- John Ericson [**(@Ericson2314)**](https://github.com/Ericson2314) +- Sergei Zimmerman [**(@xokdvium)**](https://github.com/xokdvium) +- Ruby Rose [**(@oldshensheep)**](https://github.com/oldshensheep) +- Robert Hensing [**(@roberth)**](https://github.com/roberth) +- jade [**(@lf-)**](https://github.com/lf-) +- Félix [**(@picnoir)**](https://github.com/picnoir) +- Valentin Gagarin [**(@fricklerhandwerk)**](https://github.com/fricklerhandwerk) +- Dmitry Bogatov diff --git a/doc/manual/source/release-notes/rl-2.29.md b/doc/manual/source/release-notes/rl-2.29.md new file mode 100644 index 00000000000..b59d6d6f03f --- /dev/null +++ b/doc/manual/source/release-notes/rl-2.29.md @@ -0,0 +1,160 @@ +# Release 2.29.0 (2025-05-14) + +After the special backport-based release of Nix 2.28 (timed to coincide with Nixpkgs 25.05), the release process is back to normal with 2.29. +As such, we have slightly more weeks of work from `master` (since 2.28 was branched from 2.27) than usual. +This fact is counterbalanced by the fact that most of those changes are bug fixes rather than larger new features. + +- Prettified JSON output on the terminal [#12555](https://github.com/NixOS/nix/issues/12555) [#12652](https://github.com/NixOS/nix/pull/12652) + + This makes the output easier to read. + + Scripts are mostly unaffected because for those, stdout will be a file or a pipe, not a terminal, and for those, the old single-line behavior applies. + + `--json --pretty` can be passed to enable it even if the output is not a terminal. + If your script creates a pseudoterminal for Nix's stdout, you can pass `--no-pretty` to disable the new behavior. + +- Repl: improve continuation prompt for incomplete expressions [#12846](https://github.com/NixOS/nix/pull/12846) + + Improved REPL user experience by updating the continuation prompt from invisible blank spaces to a visible `" > "`, enhancing clarity when entering multi-line expressions. + +- REPL `:load-flake` and `:reload` now work together [#8753](https://github.com/NixOS/nix/issues/8753) [#13180](https://github.com/NixOS/nix/pull/13180) + + Previously, `:reload` only reloaded the files specified with `:load` (or on the command line). + Now, it also works with the flakes specified with `:load-flake` (or on the command line). + This makes it correctly reload everything that was previously loaded, regardless of what sort of thing (plain file or flake) each item is. + +- Increase retry delays on HTTP 429 Too Many Requests [#13052](https://github.com/NixOS/nix/pull/13052) + + When downloading Nix, the retry delay was previously set to 0.25 seconds. It has now been increased to 1 minute to better handle transient CI errors, particularly on GitHub. + +- S3: opt-in the STSProfileCredentialsProvider [#12646](https://github.com/NixOS/nix/pull/12646) + + Added support for STS-based authentication for S3-based binary caches, i.e. enabling seamless integration with `aws sso login`. + +- Reduce connect timeout for http substituter [#12876](https://github.com/NixOS/nix/pull/12876) + + Previously, the Nix setting `connect-timeout` had no limit. It is now set to `5s`, offering a more practical default for users self-hosting binary caches, which may occasionally become unavailable, such as during updates. + + +- C API: functions for locking and loading a flake [#10435](https://github.com/NixOS/nix/issues/10435) [#12877](https://github.com/NixOS/nix/pull/12877) [#13098](https://github.com/NixOS/nix/pull/13098) + + This release adds functions to the C API for handling the loading of flakes. Previously, this had to be worked around by using `builtins.getFlake`. + C API consumers and language bindings now have access to basic locking functionality. + + It does not expose the full locking API, so that the implementation can evolve more freely. + Locking is controlled with the functions, which cover the common use cases for consuming a flake: + - `nix_flake_lock_flags_set_mode_check` + - `nix_flake_lock_flags_set_mode_virtual` + - `nix_flake_lock_flags_set_mode_write_as_needed` + - `nix_flake_lock_flags_add_input_override`, which also enables `virtual` + + This change also introduces the new `nix-fetchers-c` library, whose single purpose for now is to manage the (`nix.conf`) settings for the built-in fetchers. + + More details can be found in the [C API documentation](@docroot@/c-api.md). + +- No longer copy flakes that are in the nix store [#10435](https://github.com/NixOS/nix/issues/10435) [#12877](https://github.com/NixOS/nix/pull/12877) [#13098](https://github.com/NixOS/nix/pull/13098) + + Previously, we would duplicate entries like `path:/nix/store/*` back into the Nix store. + This was prominently visible for pinned system flake registry entries in NixOS, e.g., when running `nix run nixpkgs#hello`. + +- Consistently preserve error messages from cached evaluation [#12762](https://github.com/NixOS/nix/issues/12762) [#12809](https://github.com/NixOS/nix/pull/12809) + + In one code path, we are not returning the errors cached from prior evaluation, but instead throwing generic errors stemming from the lack of value (due to the error). + These generic error messages were far less informative. + Now we consistently return the original error message. + +- Faster blake3 hashing [#12676](https://github.com/NixOS/nix/pull/12676) + + The implementation for blake3 hashing is now multi-threaded and used memory-mapped IO. + Benchmark results can be found the [pull request](https://github.com/NixOS/nix/pull/12676). + +- Fix progress bar for S3 binary caches and make file transfers interruptible [#12877](https://github.com/NixOS/nix/issues/12877) [#13098](https://github.com/NixOS/nix/issues/13098) [#12538](https://github.com/NixOS/nix/pull/12538) + + The progress bar now correctly display upload/download progress for S3 up/downloads. S3 uploads are now interruptible. + +- Add host attribute of github/gitlab flakerefs to URL serialization [#12580](https://github.com/NixOS/nix/pull/12580) + + Resolved an issue where `github:` or `gitlab:` URLs lost their `host` attribute when written to a lockfile, resulting in invalid URLs. + +- Multiple signatures support in store urls [#12976](https://github.com/NixOS/nix/pull/12976) + + Added support for a `secretKeyFiles` URI parameter in Nix store URIs, allowing multiple signing key files to be specified as a comma-separated list. + This enables signing paths with multiple keys. This helps with [RFC #149](https://github.com/NixOS/rfcs/pull/149) to enable binary cache key rotation in the NixOS infra. + + Example usage: + + ```bash + nix copy --to "file:///tmp/store?secret-keys=/tmp/key1,/tmp/key2" \ + "$(nix build --print-out-paths nixpkgs#hello)" + ``` + +- nix flake show now skips over import-from-derivation [#4265](https://github.com/NixOS/nix/issues/4265) [#12583](https://github.com/NixOS/nix/pull/12583) + + Previously, if a flake contained outputs relying on [import from derivation](@docroot@/language/import-from-derivation.md) during evaluation, `nix flake show` would fail to display the rest of the flake. The updated behavior skips such outputs, allowing the rest of the flake to be shown. + +- Add `nix formatter build` and `nix formatter run` commands [#13063](https://github.com/NixOS/nix/pull/13063) + + `nix formatter run` is an alias for `nix fmt`. Nothing new there. + + `nix formatter build` is sort of like `nix build`: it builds, links, and prints a path to the formatter program: + + ``` + $ nix formatter build + /nix/store/cb9w44vkhk2x4adfxwgdkkf5gjmm856j-treefmt/bin/treefmt + ``` + + Note that unlike `nix build`, this prints the full path to the program, not just the store path (in the example above that would be `/nix/store/cb9w44vkhk2x4adfxwgdkkf5gjmm856j-treefmt`). + +- Amend OSC 8 escape stripping for xterm-style separator [#13109](https://github.com/NixOS/nix/pull/13109) + + Improve terminal escape code filtering to understand a second type of hyperlink escape codes. + This in particular prevents parts of GCC 14's diagnostics from being improperly filtered away. + + +## Contributors + + +This release was made possible by the following 40 contributors: + +- Farid Zakaria [**(@fzakaria)**](https://github.com/fzakaria) +- The Tumultuous Unicorn Of Darkness [**(@TheTumultuousUnicornOfDarkness)**](https://github.com/TheTumultuousUnicornOfDarkness) +- Robert Hensing [**(@roberth)**](https://github.com/roberth) +- Félix [**(@picnoir)**](https://github.com/picnoir) +- Valentin Gagarin [**(@fricklerhandwerk)**](https://github.com/fricklerhandwerk) +- Eelco Dolstra [**(@edolstra)**](https://github.com/edolstra) +- Vincent Breitmoser [**(@Valodim)**](https://github.com/Valodim) +- Brian McKenna [**(@puffnfresh)**](https://github.com/puffnfresh) +- ulucs [**(@ulucs)**](https://github.com/ulucs) +- John Ericson [**(@Ericson2314)**](https://github.com/Ericson2314) +- Andrey Butirsky [**(@bam80)**](https://github.com/bam80) +- Dean De Leo [**(@whatsthecraic)**](https://github.com/whatsthecraic) +- Las Safin [**(@L-as)**](https://github.com/L-as) +- Sergei Zimmerman [**(@xokdvium)**](https://github.com/xokdvium) +- Shahar "Dawn" Or [**(@mightyiam)**](https://github.com/mightyiam) +- Ryan Hendrickson [**(@rhendric)**](https://github.com/rhendric) +- Rodney Lorrimar [**(@rvl)**](https://github.com/rvl) +- Erik Nygren [**(@Kirens)**](https://github.com/Kirens) +- Cole Helbling [**(@cole-h)**](https://github.com/cole-h) +- Martin Fischer [**(@not-my-profile)**](https://github.com/not-my-profile) +- Graham Christensen [**(@grahamc)**](https://github.com/grahamc) +- Vit Gottwald [**(@VitGottwald)**](https://github.com/VitGottwald) +- silvanshade [**(@silvanshade)**](https://github.com/silvanshade) +- Illia Bobyr [**(@ilya-bobyr)**](https://github.com/ilya-bobyr) +- Jeremy Fleischman [**(@jfly)**](https://github.com/jfly) +- Ruby Rose [**(@oldshensheep)**](https://github.com/oldshensheep) +- Sergei Trofimovich [**(@trofi)**](https://github.com/trofi) +- Tim [**(@Jaculabilis)**](https://github.com/Jaculabilis) +- Anthony Wang [**(@anthowan)**](https://github.com/anthowan) +- Jörg Thalheim [**(@Mic92)**](https://github.com/Mic92) +- Sandro [**(@SuperSandro2000)**](https://github.com/SuperSandro2000) +- tomberek [**(@tomberek)**](https://github.com/tomberek) +- Dmitry Bogatov [**(@KAction)**](https://github.com/KAction) +- Sizhe Zhao [**(@Prince213)**](https://github.com/Prince213) +- jade [**(@lf-)**](https://github.com/lf-) +- Pierre-Etienne Meunier [**(@P-E-Meunier)**](https://github.com/P-E-Meunier) +- Alexander Romanov [**(@ajlekcahdp4)**](https://github.com/ajlekcahdp4) +- Domagoj MiÅ¡ković [**(@allrealmsoflife)**](https://github.com/allrealmsoflife) +- Thomas Miedema [**(@thomie)**](https://github.com/thomie) +- Yannik Sander [**(@ysndr)**](https://github.com/ysndr) +- Philipp Otterbein +- Dmitry Bogatov diff --git a/doc/manual/source/release-notes/rl-2.30.md b/doc/manual/source/release-notes/rl-2.30.md new file mode 100644 index 00000000000..34d3e5bab4c --- /dev/null +++ b/doc/manual/source/release-notes/rl-2.30.md @@ -0,0 +1,153 @@ +# Release 2.30.0 (2025-07-07) + +## Backward-incompatible changes and deprecations + +- [`build-dir`] no longer defaults to `$TMPDIR` + + The directory in which temporary build directories are created no longer defaults + to `TMPDIR` or `/tmp`, to avoid builders making their directories + world-accessible. This behavior allowed escaping the build sandbox and can + cause build impurities even when not used maliciously. We now default to `builds` + in `NIX_STATE_DIR` (which is `/nix/var/nix/builds` in the default configuration). + +- Deprecate manually making structured attrs using the `__json` attribute [#13220](https://github.com/NixOS/nix/pull/13220) + + The proper way to create a derivation using [structured attrs] in the Nix language is by using `__structuredAttrs = true` with [`builtins.derivation`]. + However, by exploiting how structured attrs are implementated, it has also been possible to create them by setting the `__json` environment variable to a serialized JSON string. + This sneaky alternative method is now deprecated, and may be disallowed in future versions of Nix. + + [structured attrs]: @docroot@/language/advanced-attributes.md#adv-attr-structuredAttrs + [`builtins.derivation`]: @docroot@/language/builtins.html#builtins-derivation + +- Rename `nix profile install` to [`nix profile add`] [#13224](https://github.com/NixOS/nix/pull/13224) + + The command `nix profile install` has been renamed to [`nix profile add`] (though the former is still available as an alias). This is because the verb "add" is a better antonym for the verb "remove" (i.e. `nix profile remove`). Nix also does not have install hooks or general behavior often associated with "installing". + +## Performance improvements + +This release has a number performance improvements, in particular: + +- Reduce the size of value from 24 to 16 bytes [#13407](https://github.com/NixOS/nix/pull/13407) + + This shaves off a very significant amount of memory used for evaluation (~20% percent reduction in maximum heap size and ~17% in total bytes). + +## Features + +- Add [stack sampling evaluation profiler] [#13220](https://github.com/NixOS/nix/pull/13220) + + The Nix evaluator now supports [stack sampling evaluation profiling](@docroot@/advanced-topics/eval-profiler.md) via the [`--eval-profiler flamegraph`] setting. + It outputs collapsed call stack information to the file specified by + [`--eval-profile-file`] (`nix.profile` by default) in a format directly consumable + by `flamegraph.pl` and compatible tools like [speedscope](https://speedscope.app/). + Sampling frequency can be configured via [`--eval-profiler-frequency`] (99 Hz by default). + + Unlike the existing [`--trace-function-calls`], this profiler includes the name of the function + being called when it's available. + +- [`nix repl`] prints which variables were loaded [#11406](https://github.com/NixOS/nix/pull/11406) + + Instead of `Added variables` it now prints the first 10 variables that were added to the global scope. + +- `nix flake archive`: Add [`--no-check-sigs`] option [#13277](https://github.com/NixOS/nix/pull/13277) + + This is useful when using [`nix flake archive`] with the destination set to a remote store. + +- Emit warnings for IFDs with [`trace-import-from-derivation`] option [#13279](https://github.com/NixOS/nix/pull/13279) + + While we have the setting [`allow-import-from-derivation`] to deny import-from-derivation (IFD), sometimes users would like to observe IFDs during CI processes to gradually phase out the idiom. The new setting `trace-import-from-derivation`, when set, logs a simple warning to the console. + +- `json-log-path` setting [#13003](https://github.com/NixOS/nix/pull/13003) + + New setting [`json-log-path`] that sends a copy of all Nix log messages (in JSON format) to a file or Unix domain socket. + +- Non-flake inputs now contain a `sourceInfo` attribute [#13164](https://github.com/NixOS/nix/issues/13164) [#13170](https://github.com/NixOS/nix/pull/13170) + + Flakes have always had a `sourceInfo` attribute which describes the source of the flake. + The `sourceInfo.outPath` is often identical to the flake's `outPath`. However, it can differ when the flake is located in a subdirectory of its source. + + Non-flake inputs (i.e. inputs with [`flake = false`]) can also be located at some path _within_ a wider source. + This usually happens when defining a relative path input within the same source as the parent flake, e.g. `inputs.foo.url = ./some-file.nix`. + Such relative inputs will now inherit their parent's `sourceInfo`. + + This also means it is now possible to use `?dir=subdir` on non-flake inputs. + + This iterates on the work done in 2.26 to improve relative path support ([#10089](https://github.com/NixOS/nix/pull/10089)), + and resolves a regression introduced in 2.28 relating to nested relative path inputs ([#13164](https://github.com/NixOS/nix/issues/13164)). + +## Miscellaneous changes + +- [`builtins.sort`] uses PeekSort [#12623](https://github.com/NixOS/nix/pull/12623) + + Previously it used libstdc++'s `std::stable_sort()`. However, that implementation is not reliable if the user-supplied comparison function is not a strict weak ordering. + +- Revert incomplete closure mixed download and build feature [#77](https://github.com/NixOS/nix/issues/77) [#12628](https://github.com/NixOS/nix/issues/12628) [#13176](https://github.com/NixOS/nix/pull/13176) + + Since Nix 1.3 ([commit `299141e`] in 2013) Nix has attempted to mix together upstream fresh builds and downstream substitutions when remote substuters contain an "incomplete closure" (have some store objects, but not the store objects they reference). + This feature is now removed. + + In the worst case, removing this feature could cause more building downstream, but it should not cause outright failures, since this is not happening for opaque store objects that we don't know how to build if we decide not to substitute. + In practice, however, we doubt even more building is very likely to happen. + Remote stores that are missing dependencies in arbitrary ways (e.g. corruption) don't seem to be very common. + + On the contrary, when remote stores fail to implement the [closure property](@docroot@/store/store-object.md#closure-property), it is usually an *intentional* choice on the part of the remote store, because it wishes to serve as an "overlay" store over another store, such as `https://cache.nixos.org`. + If an "incomplete closure" is encountered in that situation, the right fix is not to do some sort of "franken-building" as this feature implemented, but instead to make sure both substituters are enabled in the settings. + + (In the future, we should make it easier for remote stores to indicate this to clients, to catch settings that won't work in general before a missing dependency is actually encountered.) + +## Contributors + +This release was made possible by the following 32 contributors: + +- Cole Helbling [**(@cole-h)**](https://github.com/cole-h) +- Eelco Dolstra [**(@edolstra)**](https://github.com/edolstra) +- Egor Konovalov [**(@egorkonovalov)**](https://github.com/egorkonovalov) +- Farid Zakaria [**(@fzakaria)**](https://github.com/fzakaria) +- Graham Christensen [**(@grahamc)**](https://github.com/grahamc) +- gustavderdrache [**(@gustavderdrache)**](https://github.com/gustavderdrache) +- Gwenn Le Bihan [**(@gwennlbh)**](https://github.com/gwennlbh) +- h0nIg [**(@h0nIg)**](https://github.com/h0nIg) +- Jade Masker [**(@donottellmetonottellyou)**](https://github.com/donottellmetonottellyou) +- jayeshv [**(@jayeshv)**](https://github.com/jayeshv) +- Jeremy Fleischman [**(@jfly)**](https://github.com/jfly) +- John Ericson [**(@Ericson2314)**](https://github.com/Ericson2314) +- Jonas Chevalier [**(@zimbatm)**](https://github.com/zimbatm) +- Jörg Thalheim [**(@Mic92)**](https://github.com/Mic92) +- kstrafe [**(@kstrafe)**](https://github.com/kstrafe) +- Luc Perkins [**(@lucperkins)**](https://github.com/lucperkins) +- Matt Sturgeon [**(@MattSturgeon)**](https://github.com/MattSturgeon) +- Nikita Krasnov [**(@synalice)**](https://github.com/synalice) +- Peder Bergebakken Sundt [**(@pbsds)**](https://github.com/pbsds) +- pennae [**(@pennae)**](https://github.com/pennae) +- Philipp Otterbein +- Pol Dellaiera [**(@drupol)**](https://github.com/drupol) +- PopeRigby [**(@poperigby)**](https://github.com/poperigby) +- Raito Bezarius +- Robert Hensing [**(@roberth)**](https://github.com/roberth) +- Samuli Thomasson [**(@SimSaladin)**](https://github.com/SimSaladin) +- Sergei Zimmerman [**(@xokdvium)**](https://github.com/xokdvium) +- Seth Flynn [**(@getchoo)**](https://github.com/getchoo) +- Stefan Boca [**(@stefanboca)**](https://github.com/stefanboca) +- tomberek [**(@tomberek)**](https://github.com/tomberek) +- Tristan Ross [**(@RossComputerGuy)**](https://github.com/RossComputerGuy) +- Valentin Gagarin [**(@fricklerhandwerk)**](https://github.com/fricklerhandwerk) +- Vladimír ÄŒunát [**(@vcunat)**](https://github.com/vcunat) +- Wolfgang Walther [**(@wolfgangwalther)**](https://github.com/wolfgangwalther) + + +[stack sampling evaluation profiler]: @docroot@/advanced-topics/eval-profiler.md +[`--eval-profiler`]: @docroot@/command-ref/conf-file.md#conf-eval-profiler +[`--eval-profiler flamegraph`]: @docroot@/command-ref/conf-file.md#conf-eval-profiler +[`--trace-function-calls`]: @docroot@/command-ref/conf-file.md#conf-trace-function-calls +[`--eval-profile-file`]: @docroot@/command-ref/conf-file.md#conf-eval-profile-file +[`--eval-profiler-frequency`]: @docroot@/command-ref/conf-file.md#conf-eval-profiler-frequency +[`build-dir`]: @docroot@/command-ref/conf-file.md#conf-build-dir +[`nix profile add`]: @docroot@/command-ref/new-cli/nix3-profile-add.md +[`nix repl`]: @docroot@/command-ref/new-cli/nix3-repl.md +[`nix flake archive`]: @docroot@/command-ref/new-cli/nix3-flake-archive.md +[`json-log-path`]: @docroot@/command-ref/conf-file.md#conf-json-log-path +[`trace-import-from-derivation`]: @docroot@/command-ref/conf-file.md#conf-trace-import-from-derivation +[`allow-import-from-derivation`]: @docroot@/command-ref/conf-file.md#conf-allow-import-from-derivation +[`builtins.sort`]: @docroot@/language/builtins.md#builtins-sort +[`flake = false`]: @docroot@/command-ref/new-cli/nix3-flake.md?highlight=false#flake-inputs +[`--no-check-sigs`]: @docroot@/command-ref/new-cli/nix3-flake-archive.md#opt-no-check-sigs +[commit `299141e`]: https://github.com/NixOS/nix/commit/299141ecbd08bae17013226dbeae71e842b4fdd7 diff --git a/doc/manual/source/release-notes/rl-2.6.md b/doc/manual/source/release-notes/rl-2.6.md index 280faead180..f99ce4b4e82 100644 --- a/doc/manual/source/release-notes/rl-2.6.md +++ b/doc/manual/source/release-notes/rl-2.6.md @@ -13,7 +13,7 @@ * New command `nix store copy-log` to copy build logs from one store to another. * The `commit-lockfile-summary` option can be set to a non-empty - string to override the commit summary used when commiting an updated + string to override the commit summary used when committing an updated lockfile. This may be used in conjunction with the `nixConfig` attribute in `flake.nix` to better conform to repository conventions. diff --git a/doc/manual/source/store/building.md b/doc/manual/source/store/building.md new file mode 100644 index 00000000000..dbfe6b5ca10 --- /dev/null +++ b/doc/manual/source/store/building.md @@ -0,0 +1,100 @@ +# Building + +## Normalizing derivation inputs + +- Each input must be [realised] prior to building the derivation in question. + +[realised]: @docroot@/glossary.md#gloss-realise + +- Once this is done, the derivation is *normalized*, replacing each input deriving path with its store path, which we now know from realising the input. + +## Builder Execution + +The [`builder`](./derivation/index.md#builder) is executed as follows: + +- A temporary directory is created under the directory specified by + `TMPDIR` (default `/tmp`) where the build will take place. The + current directory is changed to this directory. + +- The environment is cleared and set to the derivation attributes, as + specified above. + +- In addition, the following variables are set: + + - `NIX_BUILD_TOP` contains the path of the temporary directory for + this build. + + - Also, `TMPDIR`, `TEMPDIR`, `TMP`, `TEMP` are set to point to the + temporary directory. This is to prevent the builder from + accidentally writing temporary files anywhere else. Doing so + might cause interference by other processes. + + - `PATH` is set to `/path-not-set` to prevent shells from + initialising it to their built-in default value. + + - `HOME` is set to `/homeless-shelter` to prevent programs from + using `/etc/passwd` or the like to find the user's home + directory, which could cause impurity. Usually, when `HOME` is + set, it is used as the location of the home directory, even if + it points to a non-existent path. + + - `NIX_STORE` is set to the path of the top-level Nix store + directory (typically, `/nix/store`). + + - `NIX_ATTRS_JSON_FILE` & `NIX_ATTRS_SH_FILE` if `__structuredAttrs` + is set to `true` for the derivation. A detailed explanation of this + behavior can be found in the + [section about structured attrs](@docroot@/language/advanced-attributes.md#adv-attr-structuredAttrs). + + - For each output declared in `outputs`, the corresponding + environment variable is set to point to the intended path in the + Nix store for that output. Each output path is a concatenation + of the cryptographic hash of all build inputs, the `name` + attribute and the output name. (The output name is omitted if + it’s `out`.) + +- If an output path already exists, it is removed. Also, locks are + acquired to prevent multiple [Nix instances][Nix instance] from performing the same + build at the same time. + +- A log of the combined standard output and error is written to + `/nix/var/log/nix`. + +- The builder is executed with the arguments specified by the + attribute `args`. If it exits with exit code 0, it is considered to + have succeeded. + +- The temporary directory is removed (unless the `-K` option was + specified). + +## Processing outputs + +If the builder exited successfully, the following steps happen in order to turn the output directories left behind by the builder into proper store objects: + +- **Normalize the file permissions** + + Nix sets the last-modified timestamp on all files + in the build result to 1 (00:00:01 1/1/1970 UTC), sets the group to + the default group, and sets the mode of the file to 0444 or 0555 + (i.e., read-only, with execute permission enabled if the file was + originally executable). Any possible `setuid` and `setgid` + bits are cleared. + + > **Note** + > + > Setuid and setgid programs are not currently supported by Nix. + > This is because the Nix archives used in deployment have no concept of ownership information, + > and because it makes the build result dependent on the user performing the build. + +- **Calculate the references** + + Nix scans each output path for + references to input paths by looking for the hash parts of the input + paths. Since these are potential runtime dependencies, Nix registers + them as dependencies of the output paths. + + Nix also scans for references to other outputs' paths in the same way, because outputs are allowed to refer to each other. + If the outputs' references to each other form a cycle, this is an error, because the references of store objects much be acyclic. + + +[Nix instance]: @docroot@/glossary.md#gloss-nix-instance diff --git a/doc/manual/source/store/derivation/index.md b/doc/manual/source/store/derivation/index.md new file mode 100644 index 00000000000..1687ad8c04d --- /dev/null +++ b/doc/manual/source/store/derivation/index.md @@ -0,0 +1,313 @@ +# Store Derivation and Deriving Path + +Besides functioning as a [content-addressed store], the Nix store layer works as a [build system]. +Other systems (like Git or IPFS) also store and transfer immutable data, but they don't concern themselves with *how* that data was created. + +This is where Nix distinguishes itself. +*Derivations* represent individual build steps, and *deriving paths* are needed to refer to the *outputs* of those build steps before they are built. + + +## Store Derivation {#store-derivation} + +A derivation is a specification for running an executable on precisely defined input to produce on more [store objects][store object]. +These store objects are known as the derivation's *outputs*. + +Derivations are *built*, in which case the process is spawned according to the spec, and when it exits, required to leave behind files which will (after post-processing) become the outputs of the derivation. +This process is described in detail in [Building](@docroot@/store/building.md). + + + +A derivation consists of: + + - A name + + - An [inputs specification][inputs], a set of [deriving paths][deriving path] + + - An [outputs specification][outputs], specifying which outputs should be produced, and various metadata about them. + + - The ["system" type][system] (e.g. `x86_64-linux`) where the executable is to run. + + - The [process creation fields]: to spawn the arbitrary process which will perform the build step. + +[store derivation]: #store-derivation +[inputs]: #inputs +[input]: #inputs +[outputs]: ./outputs/index.md +[output]: ./outputs/index.md +[process creation fields]: #process-creation-fields +[builder]: #builder +[args]: #args +[env]: #env +[system]: #system +[content-addressed store]: @docroot@/glossary.md#gloss-content-addressed-store +[build system]: @docroot@/glossary.md#gloss-build-system + +### Referencing derivations {#derivation-path} + +Derivations are always referred to by the [store path] of the store object they are encoded to. +See the [encoding section](#derivation-encoding) for more details on how this encoding works, and thus what exactly what store path we would end up with for a given derivation. + +The store path of the store object which encodes a derivation is often called a *derivation path* for brevity. + +## Deriving path {#deriving-path} + +Deriving paths are a way to refer to [store objects][store object] that may or may not yet be [realised][realise]. +There are two forms: + +- [*constant*]{#deriving-path-constant}: just a [store path]. + It can be made [valid][validity] by copying it into the store: from the evaluator, command line interface or another store. + +- [*output*]{#deriving-path-output}: a pair of a [store path] to a [store derivation] and an [output] name. + +In pseudo code: + +```typescript +type OutputName = String; + +type ConstantPath = { + path: StorePath; +}; + +type OutputPath = { + drvPath: StorePath; + output: OutputName; +}; + +type DerivingPath = ConstantPath | OutputPath; +``` + +Deriving paths are necessary because, in general and particularly for [content-addressing derivations][content-addressing derivation], the [store path] of an [output] is not known in advance. +We can use an output deriving path to refer to such an output, instead of the store path which we do not yet know. + +[deriving path]: #deriving-path +[validity]: @docroot@/glossary.md#gloss-validity + +## Parts of a derivation + +A derivation is constructed from the parts documented in the following subsections. + +### Inputs {#inputs} + +The inputs are a set of [deriving paths][deriving path], referring to all store objects needed in order to perform this build step. + +The [process creation fields] will presumably include many [store paths][store path]: + + - The path to the executable normally starts with a store path + - The arguments and environment variables likely contain many other store paths. + +But rather than somehow scanning all the other fields for inputs, Nix requires that all inputs be explicitly collected in the inputs field. It is instead the responsibility of the creator of a derivation (e.g. the evaluator) to ensure that every store object referenced in another field (e.g. referenced by store path) is included in this inputs field. + +### System {#system} + +The system type on which the [`builder`](#attr-builder) executable is meant to be run. + +A necessary condition for Nix to schedule a given derivation on some [Nix instance] is for the "system" of that derivation to match that instance's [`system` configuration option] or [`extra-platforms` configuration option]. + +By putting the `system` in each derivation, Nix allows *heterogenous* build plans, where not all steps can be run on the same machine or same sort of machine. +Nix can schedule builds such that it automatically builds on other platforms by [forwarding build requests](@docroot@/advanced-topics/distributed-builds.md) to other Nix instances. + +[`system` configuration option]: @docroot@/command-ref/conf-file.md#conf-system +[`extra-platforms` configuration option]: @docroot@/command-ref/conf-file.md#conf-extra-platforms + +[content-addressing derivation]: @docroot@/glossary.md#gloss-content-addressing-derivation +[realise]: @docroot@/glossary.md#gloss-realise +[store object]: @docroot@/store/store-object.md +[store path]: @docroot@/store/store-path.md + +### Process creation fields {#process-creation-fields} + +These are the three fields which describe how to spawn the process which (along with any of its own child processes) will perform the build. +You may note that this has everything needed for an `execve` system call. + +#### Builder {#builder} + +This is the path to an executable that will perform the build and produce the [outputs]. + +#### Arguments {#args} + +Command-line arguments to be passed to the [`builder`](#builder) executable. + +Note that these are the arguments after the first argument. +The first argument passed to the `builder` will be the value of `builder`, as per the usual convention on Unix. +See [Wikipedia](https://en.wikipedia.org/wiki/Argv) for details. + +#### Environment Variables {#env} + +Environment variables which will be passed to the [builder](#builder) executable. + +#### Structured Attributes {#structured-attrs} + +Nix also has special support for embedding JSON in the derivations. + +The environment variable `NIX_ATTRS_JSON_FILE` points to the exact location of that file both in a build and a [`nix-shell`](@docroot@/command-ref/nix-shell.md). + +As a convenience to Bash builders, Nix writes a script that initialises shell variables corresponding to all attributes that are representable in Bash. +The environment variable `NIX_ATTRS_SH_FILE` points to the exact location of the script, both in a build and a [`nix-shell`](@docroot@/command-ref/nix-shell.md). +This includes non-nested (associative) arrays. +For example, the attribute `hardening.format = true` ends up as the Bash associative array element `${hardening[format]}`. + +### Placeholders + +Placeholders are opaque values used within the [process creation fields] to [store objects] for which we don't yet know [store path]s. +They are strings in the form `/` that are embedded anywhere within the strings of those fields, and we are [considering](https://github.com/NixOS/nix/issues/12361) to add store-path-like placeholders. + +> **Note** +> +> Output Deriving Path exist to solve the same problem as placeholders --- that is, referring to store objects for which we don't yet know a store path. +> They also have a string syntax with `^`, [described in the encoding section](#deriving-path-encoding). +> We could use that syntax instead of `/` for placeholders, but its human-legibility would cause problems. + +There are two types of placeholder, corresponding to the two cases where this problem arises: + +- [Output placeholder]{#output-placeholder}: + + This is a placeholder for a derivation's own output. + +- [Input placeholder]{#input-placeholder}: + + This is a placeholder to a derivation's non-constant [input], + i.e. an input that is an [output derived path]. + +> **Explanation** +> +> In general, we need to [realise] a [store object] in order to be sure to have a store object for it. +> But for these two cases this is either impossible or impractical: +> +> - In the output case this is impossible: +> +> We cannot build the output until we have a correct derivation, and we cannot have a correct derivation (without using placeholders) until we have the output path. +> +> - In the input case this is impractical: +> +> If we always build a dependency first, and then refer to its output by store path, we would lose the ability for a derivation graph to describe an entire build plan consisting of multiple build steps. + +## Encoding + +### Derivation {#derivation-encoding} + +There are two formats, documented separately: + +- The legacy ["ATerm" format](@docroot@/protocols/derivation-aterm.md) + +- The experimental, currently under development and changing [JSON format](@docroot@/protocols/json/derivation.md) + +Every derivation has a canonical choice of encoding used to serialize it to a store object. +This ensures that there is a canonical [store path] used to refer to the derivation, as described in [Referencing derivations](#derivation-path). + +> **Note** +> +> Currently, the canonical encoding for every derivation is the "ATerm" format, +> but this is subject to change for the types of derivations which are not yet stable. + +Regardless of the format used, when serializing a derivation to a store object, that store object will be content-addressed. + +In the common case, the inputs to store objects are either: + + - [constant deriving paths](#deriving-path-constant) for content-addressed source objects, which are "initial inputs" rather than the outputs of some other derivation + + - the outputs of other derivations + +If those other derivations *also* abide by this common case (and likewise for transitive inputs), then the entire closure of the serialized derivation will be content-addressed. + +### Deriving Path {#deriving-path-encoding} + +- *constant* + + Constant deriving paths are encoded simply as the underlying store path is. + Thus, we see that every encoded store path is also a valid encoded (constant) deriving path. + +- *output* + + Output deriving paths are encoded by + + - encoding of a store path referring to a derivation + + - a `^` separator (or `!` in some legacy contexts) + + - the name of an output of the previously referred derivation + + > **Example** + > + > ``` + > /nix/store/lxrn8v5aamkikg6agxwdqd1jz7746wz4-firefox-98.0.2.drv^out + > ``` + > + > This parses like so: + > + > ``` + > /nix/store/lxrn8v5aamkikg6agxwdqd1jz7746wz4-firefox-98.0.2.drv^out + > |------------------------------------------------------------| |-| + > store path (usual encoding) output name + > |--| + > note the ".drv" + > ``` + +## Extending the model to be higher-order + +**Experimental feature**: [`dynamic-derivations`](@docroot@/development/experimental-features.md#xp-feature-dynamic-derivations) + +So far, we have used store paths to refer to derivations. +That works because we've implicitly assumed that all derivations are created *statically* --- created by some mechanism out of band, and then manually inserted into the store. +But what if derivations could also be created dynamically within Nix? +In other words, what if derivations could be the outputs of other derivations? + +> **Note** +> +> In the parlance of "Build Systems à la carte", we are generalizing the Nix store layer to be a "Monadic" instead of "Applicative" build system. + +How should we refer to such derivations? +A deriving path works, the same as how we refer to other derivation outputs. +But what about a dynamic derivations output? +(i.e. how do we refer to the output of a derivation, which is itself an output of a derivation?) +For that we need to generalize the definition of deriving path, replacing the store path used to refer to the derivation with a nested deriving path: + +```diff + type OutputPath = { +- drvPath: StorePath; ++ drvPath: DerivingPath; + output: OutputName; + }; +``` + +Now, the `drvPath` field of `OutputPath` is itself a `DerivingPath` instead of a `StorePath`. + +With that change, here is updated definition: + +```typescript +type OutputName = String; + +type ConstantPath = { + path: StorePath; +}; + +type OutputPath = { + drvPath: DerivingPath; + output: OutputName; +}; + +type DerivingPath = ConstantPath | OutputPath; +``` + +Under this extended model, `DerivingPath`s are thus inductively built up from a root `ConstantPath`, wrapped with zero or more outer `OutputPath`s. + +### Encoding {#deriving-path-encoding-higher-order} + +The encoding is adjusted in the natural way, encoding the `drv` field recursively using the same deriving path encoding. +The result of this is that it is possible to have a chain of `^` at the end of the final string, as opposed to just a single one. + +> **Example** +> +> ``` +> /nix/store/lxrn8v5aamkikg6agxwdqd1jz7746wz4-firefox-98.0.2.drv^foo.drv^bar.drv^out +> |----------------------------------------------------------------------------| |-| +> inner deriving path (usual encoding) output name +> |--------------------------------------------------------------------| |-----| +> even more inner deriving path (usual encoding) output name +> |------------------------------------------------------------| |-----| +> innermost constant store path (usual encoding) output name +> ``` + +[Nix instance]: @docroot@/glossary.md#gloss-nix-instance diff --git a/doc/manual/source/store/derivation/outputs/content-address.md b/doc/manual/source/store/derivation/outputs/content-address.md new file mode 100644 index 00000000000..4d51303480d --- /dev/null +++ b/doc/manual/source/store/derivation/outputs/content-address.md @@ -0,0 +1,192 @@ +# Content-addressing derivation outputs + +The content-addressing of an output only depends on that store object itself, not any other information external (such has how it was made, when it was made, etc.). +As a consequence, a store object will be content-addressed the same way regardless of whether it was manually inserted into the store, outputted by some derivation, or outputted by a some other derivation. + +The output spec for a content-addressed output must contains the following field: + +- *method*: how the data of the store object is digested into a content address + +The possible choices of *method* are described in the [section on content-addressing store objects](@docroot@/store/store-object/content-address.md). +Given the method, the output's name (computed from the derivation name and output spec mapping as described above), and the data of the store object, the output's store path will be computed as described in that section. + +## Fixed-output content-addressing {#fixed} + +In this case the content address of the *fixed* in advanced by the derivation itself. +In other words, when the derivation has finished [building](@docroot@/store/building.md), and the provisional output' content-address is computed as part of the process to turn it into a *bona fide* store object, the calculated content address must much that given in the derivation, or the build of that derivation will be deemed a failure. + +The output spec for an output with a fixed content addresses additionally contains: + +- *hash*, the hash expected from digesting the store object's file system objects. + This hash may be of a freely-chosen hash algorithm (that Nix supports) + +> **Design note** +> +> In principle, the output spec could also specify the references the store object should have, since the references and file system objects are equally parts of a content-addressed store object proper that contribute to its content-addressed. +> However, at this time, the references are not done because all fixed content-addressed outputs are required to have no references (including no self-reference). +> +> Also in principle, rather than specifying the references and file system object data with separate hashes, a single hash that constraints both could be used. +> This could be done with the final store path's digest, or better yet, the hash that will become the store path's digest before it is truncated. +> +> These possible future extensions are included to elucidate the core property of fixed-output content addressing --- that all parts of the output must be cryptographically fixed with one or more hashes --- separate from the particulars of the currently-supported store object content-addressing schemes. + +### Design rationale + +What is the purpose of fixing an output's content address in advanced? +In abstract terms, the answer is carefully controlled impurity. +Unlike a regular derivation, the [builder] executable of a derivation that produced fixed outputs has access to the network. +The outputs' guaranteed content-addresses are supposed to mitigate the risk of the builder being given these capabilities; +regardless of what the builder does *during* the build, it cannot influence downstream builds in unanticipated ways because all information it passed downstream flows through the outputs whose content-addresses are fixed. + +[builder]: @docroot@/store/derivation/index.md#builder + +In concrete terms, the purpose of this feature is fetching fixed input data like source code from the network. +For example, consider a family of "fetch URL" derivations. +These derivations download files from given URL. +To ensure that the downloaded file has not been modified, each derivation must also specify a cryptographic hash of the file. +For example, + +```jsonc +{ + "outputs: { + "out": { + "method": "nar", + "hashAlgo": "sha256", + "hash: "1md7jsfd8pa45z73bz1kszpp01yw6x5ljkjk2hx7wl800any6465", + }, + }, + "env": { + "url": "http://ftp.gnu.org/pub/gnu/hello/hello-2.1.1.tar.gz" + // ... + }, + // ... +} +``` + +It sometimes happens that the URL of the file changes, +e.g., because servers are reorganised or no longer available. +In these cases, we then must update the call to `fetchurl`, e.g., + +```diff + "env": { +- "url": "http://ftp.gnu.org/pub/gnu/hello/hello-2.1.1.tar.gz" ++ "url": "ftp://ftp.nluug.nl/pub/gnu/hello/hello-2.1.1.tar.gz" + // ... + }, +``` + +If a `fetchurl` derivation's outputs were [input-addressed][input addressing], the output paths of the derivation and of *all derivations depending on it* would change. +For instance, if we were to change the URL of the Glibc source distribution in Nixpkgs (a package on which almost all other packages depend on Linux) massive rebuilds would be needed. +This is unfortunate for a change which we know cannot have a real effect as it propagates upwards through the dependency graph. + +For content-addressed outputs (fixed or floating), on the other hand, the outputs' store path only depends on the derivation's name, data, and the `method` of the outputs' specs. +The rest of the derivation is ignored for the purpose of computing the output path. + +> **History Note** +> +> Fixed content-addressing is especially important both today and historically as the *only* form of content-addressing that is stabilized. +> This is why the rationale above contrasts it with [input addressing]. + +## (Floating) Content-Addressing {#floating} + +> **Warning** +> This is part of an [experimental feature](@docroot@/development/experimental-features.md). +> +> To use this type of output addressing, you must enable the +> [`ca-derivations`][xp-feature-ca-derivations] experimental feature. +> For example, in [nix.conf](@docroot@/command-ref/conf-file.md) you could add: +> +> ``` +> extra-experimental-features = ca-derivations +> ``` + +With this experimemental feature enabled, derivation outputs can also be content-addressed *without* fixing in the output spec what the outputs' content address must be. + +### Purity + +Because the derivation output is not fixed (just like with [input addressing]), the [builder] is not given any impure capabilities [^purity]. + +> **Configuration note** +> +> Strictly speaking, the extent to which sandboxing and deprivilaging is possible varies with the environment Nix is running in. +> Nix's configuration settings indicate what level of sandboxing is required or enabled. +> Builds of derivations will fail if they request an absence of sandboxing which is not allowed. +> Builds of derivations will also fail if the level of sandboxing specified in the configure exceeds what is possible in the given environment. +> +> (The "environment", in this case, consists of attributes such as the Operating System Nix runs atop, along with the operating-system-specific privileges that Nix has been granted. +> Because of how conventional operating systems like macos, Linux, etc. work, granting builders *fewer* privileges may ironically require that Nix be run with *more* privileges.) + +That said, derivations producing floating content-addressed outputs may declare their builders as impure (like the builders of derivations producing fixed outputs). +This is provisionally supported as part of the [`impure-derivations`][xp-feature-impure-derivations] experimental feature. + +### Compatibility negotiation + +Any derivation producing a floating content-addressed output implicitly requires the `ca-derivations` [system feature](@docroot@/command-ref/conf-file.md#conf-system-features). +This prevents scheduling the building of the derivation on a machine without the experimental feature enabled. +Even once the experimental feature is stabilized, this is still useful in order to be allow using remote builder running odler versions of Nix, or alternative implementations that do not support floating content addressing. + +### Determinism + +In the earlier [discussion of how self-references are handled when content-addressing store objects](@docroot@/store/store-object/content-address.html#self-references), it was pointed out that methods of producing store objects ought to be deterministic regardless of the choice of provisional store path. +For store objects produced by manually inserting into the store to create a store object, the "method of production" is an informally concept --- formally, Nix has no idea where the store object came from, and content-addressing is crucial in order to ensure that the derivation is *intrinsically* tamper-proof. +But for store objects produced by derivation, the "method is quite formal" --- the whole point of derivations is to be a formal notion of building, after all. +In this case, we can elevate this informal property to a formal one. + +A *deterministic* content-addressing derivation should produce outputs with the same content addresses: + +1. Every time the builder is run + + This is because either the builder is completely sandboxed, or because all any remaining impurities that leak inside the build sandbox are ignored by the builder and do not influence its behavior. + +2. Regardless of the choice of any provisional outputs paths + + Provisional store paths must be chosen for any output that has a self-reference. + The choice of provisional store path can be thought of as an impurity, since it is an arbitrary choice. + + If provisional outputs paths are deterministically chosen, we are in the first branch of part (1). + The builder the data it produces based on it in arbitrary ways, but this gets us closer to [input addressing]. + Deterministically choosing the provisional path may be considered "complete sandboxing" by removing an impurity, but this is unsatisfactory + + + + If provisional outputs paths are randomly chosen, we are in the second branch of part (1). + The builder *must* not let the random input affect the final outputs it produces, and multiple builds may be performed and the compared in order to ensure that this is in fact the case. + +### Floating versus Fixed + +While the distinction between content- and input-addressing is one of *mechanism*, the distinction between fixed and floating content addressing is more one of *policy*. +A fixed output that passes its content address check is just like a floating output. +It is only in the potential for that check to fail that they are different. + +> **Design Note** +> +> In a future world where floating content-addressing is also stable, we in principle no longer need separate [fixed](#fixed) content-addressing. +> Instead, we could always use floating content-addressing, and separately assert the precise value content address of a given store object to be used as an input (of another derivation). +> A stand-alone assertion object of this sort is not yet implemented, but its possible creation is tracked in [Issue #11955](https://github.com/NixOS/nix/issues/11955). +> +> In the current version of Nix, fixed outputs which fail their hash check are still registered as valid store objects, just not registered as outputs of the derivation which produced them. +> This is an optimization that means if the wrong output hash is specified in a derivation, and then the derivation is recreated with the right output hash, derivation does not need to be rebuilt --- avoiding downloading potentially large amounts of data twice. +> This optimisation prefigures the design above: +> If the output hash assertion was removed outside the derivation itself, Nix could additionally not only register that outputted store object like today, but could also make note that derivation did in fact successfully download some data. +For example, for the "fetch URL" example above, making such a note is tantamount to recording what data is available at the time of download at the given URL. +> It would only be when Nix subsequently tries to build something with that (refining our example) downloaded source code that Nix would be forced to check the output hash assertion, preventing it from e.g. building compromised malware. +> +> Recapping, Nix would +> +> 1. successfully download data +> 2. insert that data into the store +> 3. associate (presumably with some sort of expiration policy) the downloaded data with the derivation that downloaded it +> +> But only use the downloaded store object in subsequent derivations that depended upon the assertion if the assertion passed. +> +> This possible future extension is included to illustrate this distinction: + +[input addressing]: ./input-address.md +[xp-feature-ca-derivations]: @docroot@/development/experimental-features.md#xp-feature-ca-derivations +[xp-feature-git-hashing]: @docroot@/development/experimental-features.md#xp-feature-git-hashing +[xp-feature-impure-derivations]: @docroot@/development/experimental-features.md#xp-feature-impure-derivations diff --git a/doc/manual/source/store/derivation/outputs/index.md b/doc/manual/source/store/derivation/outputs/index.md new file mode 100644 index 00000000000..0683f5703bf --- /dev/null +++ b/doc/manual/source/store/derivation/outputs/index.md @@ -0,0 +1,97 @@ +# Derivation Outputs and Types of Derivations + +As stated on the [main pages on derivations](../index.md#store-derivation), +a derivation produces [store objects](@docroot@/store/store-object.md), which are known as the *outputs* of the derivation. +Indeed, the entire point of derivations is to produce these outputs, and to reliably and reproducibly produce these derivations each time the derivation is run. + +One of the parts of a derivation is its *outputs specification*, which specifies certain information about the outputs the derivation produces when run. +The outputs specification is a map, from names to specifications for individual outputs. + +## Output Names {#outputs} + +Output names can be any string which is also a valid [store path](@docroot@/store/store-path.md) name. +The name mapped to each output specification is not actually the name of the output. +In the general case, the output store object has name `derivationName + "-" + outputSpecName`, not any other metadata about it. +However, an output spec named "out" describes and output store object whose name is just the derivation name. + +> **Example** +> +> A derivation is named `hello`, and has two outputs, `out`, and `dev` +> +> - The derivation's path will be: `/nix/store/-hello.drv`. +> +> - The store path of `out` will be: `/nix/store/-hello`. +> +> - The store path of `dev` will be: `/nix/store/-hello-dev`. + +The outputs are the derivations are the [store objects](@docroot@/store/store-object.md) it is obligated to produce. + +> **Note** +> +> The formal terminology here is somewhat at odds with everyday communication in the Nix community today. +> "output" in casual usage tends to refer to either to the actual output store object, or the notional output spec, depending on context. +> +> For example "hello's `dev` output" means the store object referred to by the store path `/nix/store/-hello-dev`. +> It is unusual to call this the "`hello-dev` output", even though `hello-dev` is the actual name of that store object. + +## Types of output addressing + +The main information contained in an output specification is how the derivation output is addressed. +In particular, the specification decides: + +- whether the output is [content-addressed](./content-address.md) or [input-addressed](./input-address.md) + +- if the content is content-addressed, how is it content addressed + +- if the content is content-addressed, [what is its content address](./content-address.md#fixed-content-addressing) (and thus what is its [store path]) + +## Types of derivations + +The sections on each type of derivation output addressing ended up discussing other attributes of the derivation besides its outputs, such as purity, scheduling, determinism, etc. +This is no concidence; for the type of a derivation is in fact one-for-one with the type of its outputs: + +- A derivation that produces *xyz-addressed* outputs is an *xyz-addressing* derivations. + +The rules for this are fairly concise: + +- All the outputs must be of the same type / use the same addressing + + - The derivation must have at least one output + + - Additionally, if the outputs are fixed content-addressed, there must be exactly one output, whose specification is mapped from the name `out`. + (The name `out` is special, according to the rules described above. + Having only one output and calling its specification `out` means the single output is effectively anonymous; the store path just has the derivation name.) + + (This is an arbitrary restriction that could be lifted.) + +- The output is either *fixed* or *floating*, indicating whether the store path is known prior to building it. + + - With fixed content-addressing it is fixed. + + > A *fixed content-addressing* derivation is also called a *fixed-output derivation*, since that is the only currently-implemented form of fixed-output addressing + + - With floating content-addressing or input-addressing it is floating. + + > Thus, historically with Nix, with no experimental features enabled, *all* outputs are fixed. + +- The derivation may be *pure* or *impure*, indicating what read access to the outside world the [builder](../index.md#builder) has. + + - An input-addressing derivation *must* be pure. + + > If it is impure, we would have a large problem, because an input-addressed derivation always produces outputs with the same paths. + + + - A content-addressing derivation may be pure or impure + + - If it is impure, it may be fixed (typical), or it may be floating if the additional [`impure-derivations`][xp-feature-impure-derivations] experimental feature is enabled. + + - If it is pure, it must be floating. + + - Pure, fixed content-addressing derivations are not supported + + > There is no use for this forth combination. + > The sole purpose of an output's store path being fixed is to support the derivation being impure. + +[xp-feature-ca-derivations]: @docroot@/development/experimental-features.md#xp-feature-ca-derivations +[xp-feature-git-hashing]: @docroot@/development/experimental-features.md#xp-feature-git-hashing +[xp-feature-impure-derivations]: @docroot@/development/experimental-features.md#xp-feature-impure-derivations diff --git a/doc/manual/source/store/derivation/outputs/input-address.md b/doc/manual/source/store/derivation/outputs/input-address.md new file mode 100644 index 00000000000..e2e15a801b6 --- /dev/null +++ b/doc/manual/source/store/derivation/outputs/input-address.md @@ -0,0 +1,31 @@ +# Input-addressing derivation outputs + +[input addressing]: #input-addressing + +"Input addressing" means the address the store object by the *way it was made* rather than *what it is*. +That is to say, an input-addressed output's store path is a function not of the output itself, but of the derivation that produced it. +Even if two store paths have the same contents, if they are produced in different ways, and one is input-addressed, then they will have different store paths, and thus guaranteed to not be the same store object. + + + +[xp-feature-ca-derivations]: @docroot@/development/experimental-features.md#xp-feature-ca-derivations +[xp-feature-git-hashing]: @docroot@/development/experimental-features.md#xp-feature-git-hashing +[xp-feature-impure-derivations]: @docroot@/development/experimental-features.md#xp-feature-impure-derivations diff --git a/doc/manual/source/store/file-system-object/content-address.md b/doc/manual/source/store/file-system-object/content-address.md index 72b087fe982..04a1021f144 100644 --- a/doc/manual/source/store/file-system-object/content-address.md +++ b/doc/manual/source/store/file-system-object/content-address.md @@ -46,7 +46,7 @@ be many different serialisations. For these reasons, Nix has its very own archive format—the Nix Archive (NAR) format, which is carefully designed to avoid the problems described above. -The exact specification of the Nix Archive format is in `protocols/nix-archive.md` +The exact specification of the Nix Archive format is in [specified here](../../protocols/nix-archive.md). ## Content addressing File System Objects beyond a single serialisation pass @@ -80,6 +80,7 @@ Thus, Git can encode some, but not all of Nix's "File System Objects", and this In the future, we may support a Git-like hash for such file system objects, or we may adopt another Merkle DAG format which is capable of representing all Nix file system objects. + [file system object]: ../file-system-object.md [store object]: ../store-object.md [xp-feature-git-hashing]: @docroot@/development/experimental-features.md#xp-feature-git-hashing diff --git a/doc/manual/source/store/store-object.md b/doc/manual/source/store/store-object.md index caf5657d1f0..10c2384fa53 100644 --- a/doc/manual/source/store/store-object.md +++ b/doc/manual/source/store/store-object.md @@ -4,7 +4,64 @@ A Nix store is a collection of *store objects* with *references* between them. A store object consists of - A [file system object](./file-system-object.md) as data - - A set of [store paths](./store-path.md) as references to other store objects + + - A set of [store paths](./store-path.md) as references to store objects + +### References + +Store objects can refer to both other store objects and themselves. +References from a store object to itself are called *self-references*. + +Store objects and their references form a directed graph, where the store objects are the vertices, and the references are the edges. +In particular, the edge corresponding to a reference is from the store object that contains the reference, and to the store object that the store path (which is the reference) refers to. + +References other than a self-reference must not form a cycle. +The graph of references excluding self-references thus forms a [directed acyclic graph]. + +[directed acyclic graph]: @docroot@/glossary.md#gloss-directed-acyclic-graph + +We can take the [transitive closure] of the references graph, which any pair of store objects have an edge not if there is a single reference from the first to the second, but a path of one or more references from the first to the second. +The *requisites* of a store object are all store objects reachable by paths of references which start with given store object's references. + +[transitive closure]: https://en.wikipedia.org/wiki/Transitive_closure + +We can also take the [transpose graph] of the references graph, where we reverse the orientation of all edges. +The *referrers* of a store object are the store objects that reference it. + +[transpose graph]: https://en.wikipedia.org/wiki/Transpose_graph + +One can also combine both concepts: taking the transitive closure of the transposed references graph. +The *referrers closure* of a store object are the store objects that can reach the given store object via paths of references. + +> **Note** +> +> Care must be taken to distinguish between the intrinsic and extrinsic properties of store objects. +> We can create graphs from the store objects in a store, but the contents of the store is not, in general fixed, and may instead change over time. +> +> - The references of a store object --- the set of store paths called the references --- is a field of a store object, and thus intrinsic by definition. + Regardless of what store contains the store object in question, and what else that store may or may not contain, the references are the same. +> +> - The requisites of a store object are almost intrinsic --- some store paths due not precisely refer to a unique single store object. +> Exactly what store object is being referenced, and what in turn *its* references are, depends on the store in question. +> Different stores that disagree. +> +> - The referrers of a store object are completely extrinsic, and depends solely on the store which contains that store object, not the store object itself. +> Other store objects which refer to the store object in question may be added or removed from the store. + +### Immutability Store objects are [immutable](https://en.wikipedia.org/wiki/Immutable_object): -Once created, they do not change until they are deleted. +Once created, they do not change nor can any store object they reference be changed. + +> **Note** +> +> Stores which support atomically deleting multiple store objects allow more flexibility while still upholding this property. + +### Closure property + +A store can only contain a store object if it also contains all the store objects it refers to. + +> **Note** +> +> The "closure property" isn't meant to prohibit, for example, [lazy loading](https://en.wikipedia.org/wiki/Lazy_loading) of store objects. +> However, the "closure property" and immutability in conjunction imply that any such lazy loading ought to be deterministic. diff --git a/doc/manual/source/store/store-object/content-address.md b/doc/manual/source/store/store-object/content-address.md index 02dce283650..36e841fa356 100644 --- a/doc/manual/source/store/store-object/content-address.md +++ b/doc/manual/source/store/store-object/content-address.md @@ -24,13 +24,17 @@ For the full specification of the algorithms involved, see the [specification of ### File System Objects -With all currently supported store object content addressing methods, the file system object is always [content-addressed][fso-ca] first, and then that hash is incorporated into content address computation for the store object. +With all currently-supported store object content-addressing methods, the file system object is always [content-addressed][fso-ca] first, and then that hash is incorporated into content address computation for the store object. ### References +#### References to other store objects + With all currently supported store object content addressing methods, other objects are referred to by their regular (string-encoded-) [store paths][Store Path]. +#### Self-references + Self-references however cannot be referred to by their path, because we are in the midst of describing how to compute that path! > The alternative would require finding as hash function fixed point, i.e. the solution to an equation in the form @@ -40,7 +44,28 @@ Self-references however cannot be referred to by their path, because we are in t > which is computationally infeasible. > As far as we know, this is equivalent to finding a hash collision. -Instead we just have a "has self reference" boolean, which will end up affecting the digest. +Instead we have a "has self-reference" boolean, which ends up affecting the digest: +In all currently-supported store object content-addressing methods, when hashing the file system object data, any occurrence of store object's own store path in the digested data is replaced with a [sentinel value](https://en.wikipedia.org/wiki/Sentinel_value). +The hashes of these modified input streams are used instead. + +When validating the content address of a store object after the fact, the above process works as written. +However, when first creating the store object we don't know the store object's store path, as explained just above. +We therefore, strictly speaking, do not know what value we will be replacing with the sentinel value in the inputs to hash functions. +What instead happens is that the provisional store object --- the data from which we wish to create a store object --- is paired with a provisional "scratch" store path (that presumably was chosen when the data was created). +That provisional store path is instead what is replaced with the sentinel value, rather than the final store object which we do not yet know. + +> **Design note** +> +> It is an informal property of content-addressed store objects that the choice of provisional store path should not matter. +> In other words, if a provisional store object is prepared in the same way except for the choice of provision store path, the provisional data need not be identical. +> But, after the sentinel value is substituted in place of each provisional store object's provision store path, the final so-normalized data *should* be identical. +> +> If, conversely, the data after this normalization process is still different, we'll compute a different content-address. +> The method of preparing the provisional self-referenced data has *failed* to be deterministic in the sense of not *leaking* the choice of provisional store path --- a choice which is supposed to be arbitrary --- into the final store object. +> +> This property is informal because at this stage, we are just described store objects, which have no formal notion of their origin. +> Without such a formal notion, there is nothing to formally accuse of being insufficiently deterministic. +> Where we cover [derivations](@docroot@/store/derivation/index.md), we will have a chance to make this a formal property, not of content-addressed store objects themselves, but of derivations that *produce* content-addressed store objects. ### Name and Store Directory @@ -63,7 +88,7 @@ References are not supported: store objects with flat hashing *and* references c This also uses the corresponding [Flat](../file-system-object/content-address.md#serial-flat) method of file system object content addressing. -References to other store objects are supported, but self references are not. +References to other store objects are supported, but self-references are not. This is the only store-object content-addressing method that is not named identically with a corresponding file system object method. It is somewhat obscure, mainly used for "drv files" @@ -74,7 +99,7 @@ Prefer another method if possible. This uses the corresponding [Nix Archive](../file-system-object/content-address.md#serial-nix-archive) method of file system object content addressing. -References (to other store objects and self references alike) are supported so long as the hash algorithm is SHA-256, but not (neither kind) otherwise. +References (to other store objects and self-references alike) are supported so long as the hash algorithm is SHA-256, but not (neither kind) otherwise. ### Git { #method-git } diff --git a/doc/manual/substitute.py b/doc/manual/substitute.py index a8b11d93250..6e27c338818 100644 --- a/doc/manual/substitute.py +++ b/doc/manual/substitute.py @@ -57,6 +57,9 @@ def recursive_replace(data: dict[str, t.Any], book_root: Path, search_path: Path ).replace( '@docroot@', ("../" * len(path_to_chapter.parent.parts) or "./")[:-1] + ).replace( + '@_at_', + '@' ), sub_items = [ recursive_replace(sub_item, book_root, search_path) diff --git a/doc/manual/utils.nix b/doc/manual/utils.nix index 19ff49b64d9..db3a0e67a83 100644 --- a/doc/manual/utils.nix +++ b/doc/manual/utils.nix @@ -11,10 +11,15 @@ rec { concatStrings = concatStringsSep ""; - attrsToList = a: - map (name: { inherit name; value = a.${name}; }) (builtins.attrNames a); + attrsToList = + a: + map (name: { + inherit name; + value = a.${name}; + }) (builtins.attrNames a); - replaceStringsRec = from: to: string: + replaceStringsRec = + from: to: string: # recursively replace occurrences of `from` with `to` within `string` # example: # replaceStringRec "--" "-" "hello-----world" @@ -22,16 +27,18 @@ rec { let replaced = replaceStrings [ from ] [ to ] string; in - if replaced == string then string else replaceStringsRec from to replaced; + if replaced == string then string else replaceStringsRec from to replaced; toLower = replaceStrings upperChars lowerChars; squash = replaceStringsRec "\n\n\n" "\n\n"; - trim = string: + trim = + string: # trim trailing spaces and squash non-leading spaces let - trimLine = line: + trimLine = + line: let # separate leading spaces from the rest parts = split "(^ *)" line; @@ -39,19 +46,30 @@ rec { rest = elemAt parts 2; # drop trailing spaces body = head (split " *$" rest); - in spaces + replaceStringsRec " " " " body; - in concatStringsSep "\n" (map trimLine (splitLines string)); + in + spaces + replaceStringsRec " " " " body; + in + concatStringsSep "\n" (map trimLine (splitLines string)); # FIXME: O(n^2) - unique = foldl' (acc: e: if elem e acc then acc else acc ++ [ e ]) []; + unique = foldl' (acc: e: if elem e acc then acc else acc ++ [ e ]) [ ]; nameValuePair = name: value: { inherit name value; }; - filterAttrs = pred: set: - listToAttrs (concatMap (name: let v = set.${name}; in if pred name v then [(nameValuePair name v)] else []) (attrNames set)); + filterAttrs = + pred: set: + listToAttrs ( + concatMap ( + name: + let + v = set.${name}; + in + if pred name v then [ (nameValuePair name v) ] else [ ] + ) (attrNames set) + ); optionalString = cond: string: if cond then string else ""; - indent = prefix: s: - concatStringsSep "\n" (map (x: if x == "" then x else "${prefix}${x}") (splitLines s)); + indent = + prefix: s: concatStringsSep "\n" (map (x: if x == "" then x else "${prefix}${x}") (splitLines s)); } diff --git a/docker.nix b/docker.nix index e2e9da72831..410e4a178ee 100644 --- a/docker.nix +++ b/docker.nix @@ -1,21 +1,54 @@ -{ pkgs ? import { } -, lib ? pkgs.lib -, name ? "nix" -, tag ? "latest" -, bundleNixpkgs ? true -, channelName ? "nixpkgs" -, channelURL ? "https://nixos.org/channels/nixpkgs-unstable" -, extraPkgs ? [] -, maxLayers ? 100 -, nixConf ? {} -, flake-registry ? null -, uid ? 0 -, gid ? 0 -, uname ? "root" -, gname ? "root" +{ + # Core dependencies + pkgs ? import { }, + lib ? pkgs.lib, + dockerTools ? pkgs.dockerTools, + runCommand ? pkgs.runCommand, + buildPackages ? pkgs.buildPackages, + # Image configuration + name ? "nix", + tag ? "latest", + bundleNixpkgs ? true, + channelName ? "nixpkgs", + channelURL ? "https://nixos.org/channels/nixpkgs-unstable", + extraPkgs ? [ ], + maxLayers ? 70, + nixConf ? { }, + flake-registry ? null, + uid ? 0, + gid ? 0, + uname ? "root", + gname ? "root", + Labels ? { + "org.opencontainers.image.title" = "Nix"; + "org.opencontainers.image.source" = "https://github.com/NixOS/nix"; + "org.opencontainers.image.vendor" = "Nix project"; + "org.opencontainers.image.version" = nix.version; + "org.opencontainers.image.description" = "Nix container image"; + }, + Cmd ? [ (lib.getExe bashInteractive) ], + # Default Packages + nix ? pkgs.nix, + bashInteractive ? pkgs.bashInteractive, + coreutils-full ? pkgs.coreutils-full, + gnutar ? pkgs.gnutar, + gzip ? pkgs.gzip, + gnugrep ? pkgs.gnugrep, + which ? pkgs.which, + curl ? pkgs.curl, + less ? pkgs.less, + wget ? pkgs.wget, + man ? pkgs.man, + cacert ? pkgs.cacert, + findutils ? pkgs.findutils, + iana-etc ? pkgs.iana-etc, + gitMinimal ? pkgs.gitMinimal, + openssh ? pkgs.openssh, + # Other dependencies + shadow ? pkgs.shadow, }: let - defaultPkgs = with pkgs; [ + defaultPkgs = [ nix bashInteractive coreutils-full @@ -30,83 +63,80 @@ let cacert.out findutils iana-etc - git + gitMinimal openssh ] ++ extraPkgs; - users = { + users = + { - root = { - uid = 0; - shell = "${pkgs.bashInteractive}/bin/bash"; - home = "/root"; - gid = 0; - groups = [ "root" ]; - description = "System administrator"; - }; + root = { + uid = 0; + shell = lib.getExe bashInteractive; + home = "/root"; + gid = 0; + groups = [ "root" ]; + description = "System administrator"; + }; - nobody = { - uid = 65534; - shell = "${pkgs.shadow}/bin/nologin"; - home = "/var/empty"; - gid = 65534; - groups = [ "nobody" ]; - description = "Unprivileged account (don't use!)"; - }; + nobody = { + uid = 65534; + shell = lib.getExe' shadow "nologin"; + home = "/var/empty"; + gid = 65534; + groups = [ "nobody" ]; + description = "Unprivileged account (don't use!)"; + }; - } // lib.optionalAttrs (uid != 0) { - "${uname}" = { - uid = uid; - shell = "${pkgs.bashInteractive}/bin/bash"; - home = "/home/${uname}"; - gid = gid; - groups = [ "${gname}" ]; - description = "Nix user"; + } + // lib.optionalAttrs (uid != 0) { + "${uname}" = { + uid = uid; + shell = lib.getExe bashInteractive; + home = "/home/${uname}"; + gid = gid; + groups = [ "${gname}" ]; + description = "Nix user"; + }; + } + // lib.listToAttrs ( + map (n: { + name = "nixbld${toString n}"; + value = { + uid = 30000 + n; + gid = 30000; + groups = [ "nixbld" ]; + description = "Nix build user ${toString n}"; + }; + }) (lib.lists.range 1 32) + ); + + groups = + { + root.gid = 0; + nixbld.gid = 30000; + nobody.gid = 65534; + } + // lib.optionalAttrs (gid != 0) { + "${gname}".gid = gid; }; - } // lib.listToAttrs ( - map - ( - n: { - name = "nixbld${toString n}"; - value = { - uid = 30000 + n; - gid = 30000; - groups = [ "nixbld" ]; - description = "Nix build user ${toString n}"; - }; - } - ) - (lib.lists.range 1 32) - ); - - groups = { - root.gid = 0; - nixbld.gid = 30000; - nobody.gid = 65534; - } // lib.optionalAttrs (gid != 0) { - "${gname}".gid = gid; - }; userToPasswd = ( k: - { uid - , gid ? 65534 - , home ? "/var/empty" - , description ? "" - , shell ? "/bin/false" - , groups ? [ ] - }: "${k}:x:${toString uid}:${toString gid}:${description}:${home}:${shell}" - ); - passwdContents = ( - lib.concatStringsSep "\n" - (lib.attrValues (lib.mapAttrs userToPasswd users)) + { + uid, + gid ? 65534, + home ? "/var/empty", + description ? "", + shell ? "/bin/false", + groups ? [ ], + }: + "${k}:x:${toString uid}:${toString gid}:${description}:${home}:${shell}" ); + passwdContents = (lib.concatStringsSep "\n" (lib.attrValues (lib.mapAttrs userToPasswd users))); userToShadow = k: { ... }: "${k}:!:1::::::"; - shadowContents = ( - lib.concatStringsSep "\n" - (lib.attrValues (lib.mapAttrs userToShadow users)) - ); + shadowContents = (lib.concatStringsSep "\n" (lib.attrValues (lib.mapAttrs userToShadow users))); # Map groups to members # { @@ -116,107 +146,125 @@ let let # Create a flat list of user/group mappings mappings = ( - builtins.foldl' - ( - acc: user: - let - groups = users.${user}.groups or [ ]; - in - acc ++ map - (group: { - inherit user group; - }) - groups - ) - [ ] - (lib.attrNames users) + builtins.foldl' ( + acc: user: + let + groups = users.${user}.groups or [ ]; + in + acc + ++ map (group: { + inherit user group; + }) groups + ) [ ] (lib.attrNames users) ); in - ( - builtins.foldl' - ( - acc: v: acc // { - ${v.group} = acc.${v.group} or [ ] ++ [ v.user ]; - } - ) - { } - mappings) + (builtins.foldl' ( + acc: v: + acc + // { + ${v.group} = acc.${v.group} or [ ] ++ [ v.user ]; + } + ) { } mappings) ); - groupToGroup = k: { gid }: + groupToGroup = + k: + { gid }: let members = groupMemberMap.${k} or [ ]; in "${k}:x:${toString gid}:${lib.concatStringsSep "," members}"; - groupContents = ( - lib.concatStringsSep "\n" - (lib.attrValues (lib.mapAttrs groupToGroup groups)) - ); - - defaultNixConf = { - sandbox = "false"; - build-users-group = "nixbld"; - trusted-public-keys = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" ]; - }; + groupContents = (lib.concatStringsSep "\n" (lib.attrValues (lib.mapAttrs groupToGroup groups))); + + toConf = + with pkgs.lib.generators; + toKeyValue { + mkKeyValue = mkKeyValueDefault { + mkValueString = v: if lib.isList v then lib.concatStringsSep " " v else mkValueStringDefault { } v; + } " = "; + }; - nixConfContents = (lib.concatStringsSep "\n" (lib.mapAttrsFlatten (n: v: - let - vStr = if builtins.isList v then lib.concatStringsSep " " v else v; - in - "${n} = ${vStr}") (defaultNixConf // nixConf))) + "\n"; + nixConfContents = toConf ( + { + sandbox = false; + build-users-group = "nixbld"; + trusted-public-keys = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" ]; + } + // nixConf + ); userHome = if uid == 0 then "/root" else "/home/${uname}"; baseSystem = let nixpkgs = pkgs.path; - channel = pkgs.runCommand "channel-nixos" { inherit bundleNixpkgs; } '' + channel = runCommand "channel-nixos" { inherit bundleNixpkgs; } '' mkdir $out if [ "$bundleNixpkgs" ]; then - ln -s ${nixpkgs} $out/nixpkgs + ln -s ${ + builtins.path { + path = nixpkgs; + name = "source"; + } + } $out/nixpkgs echo "[]" > $out/manifest.nix fi ''; - rootEnv = pkgs.buildPackages.buildEnv { - name = "root-profile-env"; - paths = defaultPkgs; - }; - manifest = pkgs.buildPackages.runCommand "manifest.nix" { } '' + # doc/manual/source/command-ref/files/manifest.nix.md + manifest = buildPackages.runCommand "manifest.nix" { } '' cat > $out < $out/etc/passwd - echo "" >> $out/etc/passwd + cat $passwdContentsPath > $out/etc/passwd + echo "" >> $out/etc/passwd - cat $groupContentsPath > $out/etc/group - echo "" >> $out/etc/group + cat $groupContentsPath > $out/etc/group + echo "" >> $out/etc/group - cat $shadowContentsPath > $out/etc/shadow - echo "" >> $out/etc/shadow + cat $shadowContentsPath > $out/etc/shadow + echo "" >> $out/etc/shadow - mkdir -p $out/usr - ln -s /nix/var/nix/profiles/share $out/usr/ + mkdir -p $out/usr + ln -s /nix/var/nix/profiles/share $out/usr/ - mkdir -p $out/nix/var/nix/gcroots + mkdir -p $out/nix/var/nix/gcroots - mkdir $out/tmp + mkdir $out/tmp - mkdir -p $out/var/tmp + mkdir -p $out/var/tmp - mkdir -p $out/etc/nix - cat $nixConfContentsPath > $out/etc/nix/nix.conf + mkdir -p $out/etc/nix + cat $nixConfContentsPath > $out/etc/nix/nix.conf - mkdir -p $out${userHome} - mkdir -p $out/nix/var/nix/profiles/per-user/${uname} + mkdir -p $out${userHome} + mkdir -p $out/nix/var/nix/profiles/per-user/${uname} - ln -s ${profile} $out/nix/var/nix/profiles/default-1-link - ln -s /nix/var/nix/profiles/default-1-link $out/nix/var/nix/profiles/default - ln -s /nix/var/nix/profiles/default $out${userHome}/.nix-profile + # see doc/manual/source/command-ref/files/profiles.md + ln -s ${profile} $out/nix/var/nix/profiles/default-1-link + ln -s /nix/var/nix/profiles/default-1-link $out/nix/var/nix/profiles/default - ln -s ${channel} $out/nix/var/nix/profiles/per-user/${uname}/channels-1-link - ln -s /nix/var/nix/profiles/per-user/${uname}/channels-1-link $out/nix/var/nix/profiles/per-user/${uname}/channels + # see doc/manual/source/command-ref/files/channels.md + ln -s ${channel} $out/nix/var/nix/profiles/per-user/${uname}/channels-1-link + ln -s /nix/var/nix/profiles/per-user/${uname}/channels-1-link $out/nix/var/nix/profiles/per-user/${uname}/channels - mkdir -p $out${userHome}/.nix-defexpr - ln -s /nix/var/nix/profiles/per-user/${uname}/channels $out${userHome}/.nix-defexpr/channels - echo "${channelURL} ${channelName}" > $out${userHome}/.nix-channels + # see doc/manual/source/command-ref/files/default-nix-expression.md + mkdir -p $out${userHome}/.nix-defexpr + ln -s /nix/var/nix/profiles/per-user/${uname}/channels $out${userHome}/.nix-defexpr/channels + echo "${channelURL} ${channelName}" > $out${userHome}/.nix-channels - mkdir -p $out/bin $out/usr/bin - ln -s ${pkgs.coreutils}/bin/env $out/usr/bin/env - ln -s ${pkgs.bashInteractive}/bin/bash $out/bin/sh + # may get replaced by pkgs.dockerTools.binSh & pkgs.dockerTools.usrBinEnv + mkdir -p $out/bin $out/usr/bin + ln -s ${lib.getExe' coreutils-full "env"} $out/usr/bin/env + ln -s ${lib.getExe bashInteractive} $out/bin/sh - '' + (lib.optionalString (flake-registry-path != null) '' - nixCacheDir="${userHome}/.cache/nix" - mkdir -p $out$nixCacheDir - globalFlakeRegistryPath="$nixCacheDir/flake-registry.json" - ln -s ${flake-registry-path} $out$globalFlakeRegistryPath - mkdir -p $out/nix/var/nix/gcroots/auto - rootName=$(${pkgs.nix}/bin/nix --extra-experimental-features nix-command hash file --type sha1 --base32 <(echo -n $globalFlakeRegistryPath)) - ln -s $globalFlakeRegistryPath $out/nix/var/nix/gcroots/auto/$rootName - '')); + '' + + (lib.optionalString (flake-registry-path != null) '' + nixCacheDir="${userHome}/.cache/nix" + mkdir -p $out$nixCacheDir + globalFlakeRegistryPath="$nixCacheDir/flake-registry.json" + ln -s ${flake-registry-path} $out$globalFlakeRegistryPath + mkdir -p $out/nix/var/nix/gcroots/auto + rootName=$(${lib.getExe' nix "nix"} --extra-experimental-features nix-command hash file --type sha1 --base32 <(echo -n $globalFlakeRegistryPath)) + ln -s $globalFlakeRegistryPath $out/nix/var/nix/gcroots/auto/$rootName + '') + ); in -pkgs.dockerTools.buildLayeredImageWithNixDb { - - inherit name tag maxLayers uid gid uname gname; +dockerTools.buildLayeredImageWithNixDb { + + inherit + name + tag + maxLayers + uid + gid + uname + gname + ; contents = [ baseSystem ]; @@ -301,19 +365,23 @@ pkgs.dockerTools.buildLayeredImageWithNixDb { ''; config = { - Cmd = [ "${userHome}/.nix-profile/bin/bash" ]; + inherit Cmd Labels; User = "${toString uid}:${toString gid}"; Env = [ "USER=${uname}" - "PATH=${lib.concatStringsSep ":" [ - "${userHome}/.nix-profile/bin" - "/nix/var/nix/profiles/default/bin" - "/nix/var/nix/profiles/default/sbin" - ]}" - "MANPATH=${lib.concatStringsSep ":" [ - "${userHome}/.nix-profile/share/man" - "/nix/var/nix/profiles/default/share/man" - ]}" + "PATH=${ + lib.concatStringsSep ":" [ + "${userHome}/.nix-profile/bin" + "/nix/var/nix/profiles/default/bin" + "/nix/var/nix/profiles/default/sbin" + ] + }" + "MANPATH=${ + lib.concatStringsSep ":" [ + "${userHome}/.nix-profile/share/man" + "/nix/var/nix/profiles/default/share/man" + ] + }" "SSL_CERT_FILE=/nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt" "GIT_SSL_CAINFO=/nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt" "NIX_SSL_CERT_FILE=/nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt" diff --git a/flake.lock b/flake.lock index b5d0b881c5c..3075eabc233 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1696426674, - "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", "owner": "edolstra", "repo": "flake-compat", - "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", "type": "github" }, "original": { @@ -23,11 +23,11 @@ ] }, "locked": { - "lastModified": 1719994518, - "narHash": "sha256-pQMhCCHyQGRzdfAkdJ4cIWiw+JNuWsTX7f0ZYSyz0VY=", + "lastModified": 1733312601, + "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "9227223f6d922fee3c7b190b2cc238a99527bbb7", + "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", "type": "github" }, "original": { @@ -48,11 +48,11 @@ ] }, "locked": { - "lastModified": 1721042469, - "narHash": "sha256-6FPUl7HVtvRHCCBQne7Ylp4p+dpP3P/OYuzjztZ4s70=", + "lastModified": 1734279981, + "narHash": "sha256-NdaCraHPp8iYMWzdXAt5Nv6sA3MUzlCiGiR586TCwo0=", "owner": "cachix", "repo": "git-hooks.nix", - "rev": "f451c19376071a90d8c58ab1a953c6e9840527fd", + "rev": "aa9f40c906904ebd83da78e7f328cd8aeaeae785", "type": "github" }, "original": { @@ -61,35 +61,18 @@ "type": "github" } }, - "libgit2": { - "flake": false, - "locked": { - "lastModified": 1715853528, - "narHash": "sha256-J2rCxTecyLbbDdsyBWn9w7r3pbKRMkI9E7RvRgAqBdY=", - "owner": "libgit2", - "repo": "libgit2", - "rev": "36f7e21ad757a3dacc58cf7944329da6bc1d6e96", - "type": "github" - }, - "original": { - "owner": "libgit2", - "ref": "v1.8.1", - "repo": "libgit2", - "type": "github" - } - }, "nixpkgs": { "locked": { - "lastModified": 1723688146, - "narHash": "sha256-sqLwJcHYeWLOeP/XoLwAtYjr01TISlkOfz+NG82pbdg=", + "lastModified": 1747179050, + "narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "c3d4ac725177c030b1e289015989da2ad9d56af0", + "rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-24.05", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } @@ -131,7 +114,6 @@ "flake-compat": "flake-compat", "flake-parts": "flake-parts", "git-hooks-nix": "git-hooks-nix", - "libgit2": "libgit2", "nixpkgs": "nixpkgs", "nixpkgs-23-11": "nixpkgs-23-11", "nixpkgs-regression": "nixpkgs-regression" diff --git a/flake.nix b/flake.nix index 794736af4ec..69bd2a21adb 100644 --- a/flake.nix +++ b/flake.nix @@ -1,11 +1,14 @@ { description = "The purely functional package manager"; - inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + inputs.nixpkgs-regression.url = "github:NixOS/nixpkgs/215d4d0fd80ca5163643b03a33fde804a29cc1e2"; inputs.nixpkgs-23-11.url = "github:NixOS/nixpkgs/a62e6edd6d5e1fa0329b8653c801147986f8d446"; - inputs.flake-compat = { url = "github:edolstra/flake-compat"; flake = false; }; - inputs.libgit2 = { url = "github:libgit2/libgit2/v1.8.1"; flake = false; }; + inputs.flake-compat = { + url = "github:edolstra/flake-compat"; + flake = false; + }; # dev tooling inputs.flake-parts.url = "github:hercules-ci/flake-parts"; @@ -18,8 +21,13 @@ inputs.git-hooks-nix.inputs.flake-compat.follows = ""; inputs.git-hooks-nix.inputs.gitignore.follows = ""; - outputs = inputs@{ self, nixpkgs, nixpkgs-regression, libgit2, ... }: - + outputs = + inputs@{ + self, + nixpkgs, + nixpkgs-regression, + ... + }: let inherit (nixpkgs) lib; @@ -27,16 +35,23 @@ officialRelease = false; linux32BitSystems = [ "i686-linux" ]; - linux64BitSystems = [ "x86_64-linux" "aarch64-linux" ]; + linux64BitSystems = [ + "x86_64-linux" + "aarch64-linux" + ]; linuxSystems = linux32BitSystems ++ linux64BitSystems; - darwinSystems = [ "x86_64-darwin" "aarch64-darwin" ]; + darwinSystems = [ + "x86_64-darwin" + "aarch64-darwin" + ]; systems = linuxSystems ++ darwinSystems; crossSystems = [ "armv6l-unknown-linux-gnueabihf" "armv7l-unknown-linux-gnueabihf" "riscv64-unknown-linux-gnu" - "x86_64-unknown-netbsd" + # Disabled because of https://github.com/NixOS/nixpkgs/issues/344423 + # "x86_64-unknown-netbsd" "x86_64-unknown-freebsd" "x86_64-w64-mingw32" ]; @@ -58,62 +73,66 @@ (Provided that the names are unique.) See https://nixos.org/manual/nixpkgs/stable/index.html#function-library-lib.attrsets.concatMapAttrs - */ + */ flatMapAttrs = attrs: f: lib.concatMapAttrs f attrs; forAllSystems = lib.genAttrs systems; forAllCrossSystems = lib.genAttrs crossSystems; - forAllStdenvs = f: - lib.listToAttrs - (map - (stdenvName: { - name = "${stdenvName}Packages"; - value = f stdenvName; - }) - stdenvs); - + forAllStdenvs = lib.genAttrs stdenvs; # We don't apply flake-parts to the whole flake so that non-development attributes # load without fetching any development inputs. devFlake = inputs.flake-parts.lib.mkFlake { inherit inputs; } { imports = [ ./maintainers/flake-module.nix ]; systems = lib.subtractLists crossSystems systems; - perSystem = { system, ... }: { - _module.args.pkgs = nixpkgsFor.${system}.native; - }; + perSystem = + { system, ... }: + { + _module.args.pkgs = nixpkgsFor.${system}.native; + }; }; # Memoize nixpkgs for different platforms for efficiency. - nixpkgsFor = forAllSystems - (system: let - make-pkgs = crossSystem: stdenv: import nixpkgs { - localSystem = { - inherit system; - }; - crossSystem = if crossSystem == null then null else { - config = crossSystem; - } // lib.optionalAttrs (crossSystem == "x86_64-unknown-freebsd13") { - useLLVM = true; - }; - overlays = [ - (overlayFor (p: p.${stdenv})) - ]; - }; - stdenvs = forAllStdenvs (make-pkgs null); - native = stdenvs.stdenvPackages; - in { - inherit stdenvs native; - static = native.pkgsStatic; - cross = forAllCrossSystems (crossSystem: make-pkgs crossSystem "stdenv"); - }); - - binaryTarball = nix: pkgs: pkgs.callPackage ./scripts/binary-tarball.nix { - inherit nix; - }; + nixpkgsFor = forAllSystems ( + system: + let + make-pkgs = + crossSystem: + forAllStdenvs ( + stdenv: + import nixpkgs { + localSystem = { + inherit system; + }; + crossSystem = + if crossSystem == null then + null + else + { + config = crossSystem; + } + // lib.optionalAttrs (crossSystem == "x86_64-unknown-freebsd13") { + useLLVM = true; + }; + overlays = [ + (overlayFor (pkgs: pkgs.${stdenv})) + ]; + } + ); + in + rec { + nativeForStdenv = make-pkgs null; + crossForStdenv = forAllCrossSystems make-pkgs; + # Alias for convenience + native = nativeForStdenv.stdenv; + cross = forAllCrossSystems (crossSystem: crossForStdenv.${crossSystem}.stdenv); + } + ); - overlayFor = getStdenv: final: prev: + overlayFor = + getStdenv: final: prev: let stdenv = getStdenv final; in @@ -124,49 +143,46 @@ # without "polluting" the top level "`pkgs`" attrset. # This also has the benefit of providing us with a distinct set of packages # we can iterate over. - nixComponents = + # The `2` suffix is here because otherwise it interferes with `nixVersions.latest`, which is used in daemon compat tests. + nixComponents2 = lib.makeScopeWithSplicing' { inherit (final) splicePackages; - inherit (final.nixDependencies) newScope; + inherit (final.nixDependencies2) newScope; } { - otherSplices = final.generateSplicesForMkScope "nixComponents"; + otherSplices = final.generateSplicesForMkScope "nixComponents2"; f = import ./packaging/components.nix { inherit (final) lib; inherit officialRelease; + pkgs = final; src = self; + maintainers = [ ]; }; }; # The dependencies are in their own scope, so that they don't have to be - # in Nixpkgs top level `pkgs` or `nixComponents`. - nixDependencies = + # in Nixpkgs top level `pkgs` or `nixComponents2`. + # The `2` suffix is here because otherwise it interferes with `nixVersions.latest`, which is used in daemon compat tests. + nixDependencies2 = lib.makeScopeWithSplicing' { inherit (final) splicePackages; - inherit (final) newScope; # layered directly on pkgs, unlike nixComponents above + inherit (final) newScope; # layered directly on pkgs, unlike nixComponents2 above } { - otherSplices = final.generateSplicesForMkScope "nixDependencies"; + otherSplices = final.generateSplicesForMkScope "nixDependencies2"; f = import ./packaging/dependencies.nix { inherit inputs stdenv; pkgs = final; }; }; - nix = final.nixComponents.nix-cli; - - # See https://github.com/NixOS/nixpkgs/pull/214409 - # Remove when fixed in this flake's nixpkgs - pre-commit = - if prev.stdenv.hostPlatform.system == "i686-linux" - then (prev.pre-commit.override (o: { dotnet-sdk = ""; })).overridePythonAttrs (o: { doCheck = false; }) - else prev.pre-commit; - + nix = final.nixComponents2.nix-cli; }; - in { + in + { # A Nixpkgs overlay that overrides the 'nix' and # 'nix-perl-bindings' packages. overlays.default = overlayFor (p: p.stdenv); @@ -174,7 +190,6 @@ hydraJobs = import ./packaging/hydra.nix { inherit inputs - binaryTarball forAllCrossSystems forAllSystems lib @@ -185,151 +200,270 @@ ; }; - checks = forAllSystems (system: { - binaryTarball = self.hydraJobs.binaryTarball.${system}; - installTests = self.hydraJobs.installTests.${system}; - nixpkgsLibTests = self.hydraJobs.tests.nixpkgsLibTests.${system}; - rl-next = - let pkgs = nixpkgsFor.${system}.native; - in pkgs.buildPackages.runCommand "test-rl-next-release-notes" { } '' - LANG=C.UTF-8 ${pkgs.changelog-d}/bin/changelog-d ${./doc/manual/rl-next} >$out - ''; - repl-completion = nixpkgsFor.${system}.native.callPackage ./tests/repl-completion.nix { }; - } // (lib.optionalAttrs (builtins.elem system linux64BitSystems)) { - dockerImage = self.hydraJobs.dockerImage.${system}; - } // (lib.optionalAttrs (!(builtins.elem system linux32BitSystems))) { - # Some perl dependencies are broken on i686-linux. - # Since the support is only best-effort there, disable the perl - # bindings - - # Temporarily disabled because GitHub Actions OOM issues. Once - # the old build system is gone and we are back to one build - # system, we should reenable this. - #perlBindings = self.hydraJobs.perlBindings.${system}; - } - # Add "passthru" tests - // flatMapAttrs ({ - "" = nixpkgsFor.${system}.native; - } // lib.optionalAttrs (! nixpkgsFor.${system}.native.stdenv.hostPlatform.isDarwin) { - # TODO: enable static builds for darwin, blocked on: - # https://github.com/NixOS/nixpkgs/issues/320448 - # TODO: disabled to speed up GHA CI. - #"static-" = nixpkgsFor.${system}.static; - }) - (nixpkgsPrefix: nixpkgs: - flatMapAttrs nixpkgs.nixComponents - (pkgName: pkg: - flatMapAttrs pkg.tests or {} - (testName: test: { - "${nixpkgsPrefix}${pkgName}-${testName}" = test; - }) + checks = forAllSystems ( + system: + { + installerScriptForGHA = self.hydraJobs.installerScriptForGHA.${system}; + installTests = self.hydraJobs.installTests.${system}; + nixpkgsLibTests = self.hydraJobs.tests.nixpkgsLibTests.${system}; + rl-next = + let + pkgs = nixpkgsFor.${system}.native; + in + pkgs.buildPackages.runCommand "test-rl-next-release-notes" { } '' + LANG=C.UTF-8 ${pkgs.changelog-d}/bin/changelog-d ${./doc/manual/rl-next} >$out + ''; + repl-completion = nixpkgsFor.${system}.native.callPackage ./tests/repl-completion.nix { }; + + /** + Checks for our packaging expressions. + This shouldn't build anything significant; just check that things + (including derivations) are _set up_ correctly. + */ + packaging-overriding = + let + pkgs = nixpkgsFor.${system}.native; + nix = self.packages.${system}.nix; + in + assert (nix.appendPatches [ pkgs.emptyFile ]).libs.nix-util.src.patches == [ pkgs.emptyFile ]; + if pkgs.stdenv.buildPlatform.isDarwin then + lib.warn "packaging-overriding check currently disabled because of a permissions issue on macOS" pkgs.emptyFile + else + # If this fails, something might be wrong with how we've wired the scope, + # or something could be broken in Nixpkgs. + pkgs.testers.testEqualContents { + assertion = "trivial patch does not change source contents"; + expected = "${./.}"; + actual = + # Same for all components; nix-util is an arbitrary pick + (nix.appendPatches [ pkgs.emptyFile ]).libs.nix-util.src; + }; + } + // (lib.optionalAttrs (builtins.elem system linux64BitSystems)) { + dockerImage = self.hydraJobs.dockerImage.${system}; + } + // (lib.optionalAttrs (!(builtins.elem system linux32BitSystems))) { + # Some perl dependencies are broken on i686-linux. + # Since the support is only best-effort there, disable the perl + # bindings + perlBindings = self.hydraJobs.perlBindings.${system}; + } + # Add "passthru" tests + // + flatMapAttrs + ( + { + # Run all tests with UBSAN enabled. Running both with ubsan and + # without doesn't seem to have much immediate benefit for doubling + # the GHA CI workaround. + # + # TODO: Work toward enabling "address,undefined" if it seems feasible. + # This would maybe require dropping Boost coroutines and ignoring intentional + # memory leaks with detect_leaks=0. + "" = rec { + nixpkgs = nixpkgsFor.${system}.native; + nixComponents = nixpkgs.nixComponents2.overrideScope ( + nixCompFinal: nixCompPrev: { + mesonComponentOverrides = _finalAttrs: prevAttrs: { + mesonFlags = + (prevAttrs.mesonFlags or [ ]) + # TODO: Macos builds instrumented with ubsan take very long + # to run functional tests. + ++ lib.optionals (!nixpkgs.stdenv.hostPlatform.isDarwin) [ + (lib.mesonOption "b_sanitize" "undefined") + ]; + }; + } + ); + }; + } + // lib.optionalAttrs (!nixpkgsFor.${system}.native.stdenv.hostPlatform.isDarwin) { + # TODO: enable static builds for darwin, blocked on: + # https://github.com/NixOS/nixpkgs/issues/320448 + # TODO: disabled to speed up GHA CI. + # "static-" = { + # nixpkgs = nixpkgsFor.${system}.native.pkgsStatic; + # }; + } ) - // lib.optionalAttrs (nixpkgs.stdenv.hostPlatform == nixpkgs.stdenv.buildPlatform) { - "${nixpkgsPrefix}nix-functional-tests" = nixpkgs.nixComponents.nix-functional-tests; - } - ) - // devFlake.checks.${system} or {} + ( + nixpkgsPrefix: + { + nixpkgs, + nixComponents ? nixpkgs.nixComponents2, + }: + flatMapAttrs nixComponents ( + pkgName: pkg: + flatMapAttrs pkg.tests or { } ( + testName: test: { + "${nixpkgsPrefix}${pkgName}-${testName}" = test; + } + ) + ) + // lib.optionalAttrs (nixpkgs.stdenv.hostPlatform == nixpkgs.stdenv.buildPlatform) { + "${nixpkgsPrefix}nix-functional-tests" = nixComponents.nix-functional-tests; + } + ) + // devFlake.checks.${system} or { } ); - packages = forAllSystems (system: - { # Here we put attributes that map 1:1 into packages., ie + packages = forAllSystems ( + system: + { + # Here we put attributes that map 1:1 into packages., ie # for which we don't apply the full build matrix such as cross or static. inherit (nixpkgsFor.${system}.native) - changelog-d; + changelog-d + ; default = self.packages.${system}.nix; + installerScriptForGHA = self.hydraJobs.installerScriptForGHA.${system}; + binaryTarball = self.hydraJobs.binaryTarball.${system}; # TODO probably should be `nix-cli` nix = self.packages.${system}.nix-everything; - nix-manual = nixpkgsFor.${system}.native.nixComponents.nix-manual; - nix-internal-api-docs = nixpkgsFor.${system}.native.nixComponents.nix-internal-api-docs; - nix-external-api-docs = nixpkgsFor.${system}.native.nixComponents.nix-external-api-docs; + nix-manual = nixpkgsFor.${system}.native.nixComponents2.nix-manual; + nix-internal-api-docs = nixpkgsFor.${system}.native.nixComponents2.nix-internal-api-docs; + nix-external-api-docs = nixpkgsFor.${system}.native.nixComponents2.nix-external-api-docs; } # We need to flatten recursive attribute sets of derivations to pass `flake check`. - // flatMapAttrs - { # Components we'll iterate over in the upcoming lambda - "nix-util" = { }; - "nix-util-c" = { }; - "nix-util-test-support" = { }; - "nix-util-tests" = { }; + // + flatMapAttrs + { + # Components we'll iterate over in the upcoming lambda + "nix-util" = { }; + "nix-util-c" = { }; + "nix-util-test-support" = { }; + "nix-util-tests" = { }; - "nix-store" = { }; - "nix-store-c" = { }; - "nix-store-test-support" = { }; - "nix-store-tests" = { }; + "nix-store" = { }; + "nix-store-c" = { }; + "nix-store-test-support" = { }; + "nix-store-tests" = { }; - "nix-fetchers" = { }; - "nix-fetchers-tests" = { }; + "nix-fetchers" = { }; + "nix-fetchers-c" = { }; + "nix-fetchers-tests" = { }; - "nix-expr" = { }; - "nix-expr-c" = { }; - "nix-expr-test-support" = { }; - "nix-expr-tests" = { }; + "nix-expr" = { }; + "nix-expr-c" = { }; + "nix-expr-test-support" = { }; + "nix-expr-tests" = { }; - "nix-flake" = { }; - "nix-flake-tests" = { }; + "nix-flake" = { }; + "nix-flake-c" = { }; + "nix-flake-tests" = { }; - "nix-main" = { }; - "nix-main-c" = { }; + "nix-main" = { }; + "nix-main-c" = { }; - "nix-cmd" = { }; + "nix-cmd" = { }; - "nix-cli" = { }; + "nix-cli" = { }; - "nix-everything" = { }; + "nix-everything" = { }; - "nix-functional-tests" = { supportsCross = false; }; + "nix-functional-tests" = { + supportsCross = false; + }; - "nix-perl-bindings" = { supportsCross = false; }; - } - (pkgName: { supportsCross ? true }: { - # These attributes go right into `packages.`. - "${pkgName}" = nixpkgsFor.${system}.native.nixComponents.${pkgName}; - "${pkgName}-static" = nixpkgsFor.${system}.static.nixComponents.${pkgName}; + "nix-perl-bindings" = { + supportsCross = false; + }; } - // lib.optionalAttrs supportsCross (flatMapAttrs (lib.genAttrs crossSystems (_: { })) (crossSystem: {}: { - # These attributes go right into `packages.`. - "${pkgName}-${crossSystem}" = nixpkgsFor.${system}.cross.${crossSystem}.nixComponents.${pkgName}; - })) - // flatMapAttrs (lib.genAttrs stdenvs (_: { })) (stdenvName: {}: { - # These attributes go right into `packages.`. - "${pkgName}-${stdenvName}" = nixpkgsFor.${system}.stdenvs."${stdenvName}Packages".nixComponents.${pkgName}; - }) - ) + ( + pkgName: + { + supportsCross ? true, + }: + { + # These attributes go right into `packages.`. + "${pkgName}" = nixpkgsFor.${system}.native.nixComponents2.${pkgName}; + "${pkgName}-static" = nixpkgsFor.${system}.native.pkgsStatic.nixComponents2.${pkgName}; + "${pkgName}-llvm" = nixpkgsFor.${system}.native.pkgsLLVM.nixComponents2.${pkgName}; + } + // lib.optionalAttrs supportsCross ( + flatMapAttrs (lib.genAttrs crossSystems (_: { })) ( + crossSystem: + { }: + { + # These attributes go right into `packages.`. + "${pkgName}-${crossSystem}" = nixpkgsFor.${system}.cross.${crossSystem}.nixComponents2.${pkgName}; + } + ) + ) + // flatMapAttrs (lib.genAttrs stdenvs (_: { })) ( + stdenvName: + { }: + { + # These attributes go right into `packages.`. + "${pkgName}-${stdenvName}" = + nixpkgsFor.${system}.nativeForStdenv.${stdenvName}.nixComponents2.${pkgName}; + } + ) + ) // lib.optionalAttrs (builtins.elem system linux64BitSystems) { - dockerImage = - let - pkgs = nixpkgsFor.${system}.native; - image = import ./docker.nix { inherit pkgs; tag = pkgs.nix.version; }; - in - pkgs.runCommand - "docker-image-tarball-${pkgs.nix.version}" - { meta.description = "Docker image with Nix for ${system}"; } - '' - mkdir -p $out/nix-support - image=$out/image.tar.gz - ln -s ${image} $image - echo "file binary-dist $image" >> $out/nix-support/hydra-build-products - ''; - }); - - devShells = let - makeShell = import ./packaging/dev-shell.nix { inherit lib devFlake; }; - prefixAttrs = prefix: lib.concatMapAttrs (k: v: { "${prefix}-${k}" = v; }); - in - forAllSystems (system: - prefixAttrs "native" (forAllStdenvs (stdenvName: makeShell { - pkgs = nixpkgsFor.${system}.stdenvs."${stdenvName}Packages"; - })) // - lib.optionalAttrs (!nixpkgsFor.${system}.native.stdenv.isDarwin) ( - prefixAttrs "static" (forAllStdenvs (stdenvName: makeShell { - pkgs = nixpkgsFor.${system}.stdenvs."${stdenvName}Packages".pkgsStatic; - })) // - prefixAttrs "cross" (forAllCrossSystems (crossSystem: makeShell { - pkgs = nixpkgsFor.${system}.cross.${crossSystem}; - })) - ) // - { - default = self.devShells.${system}.native-stdenvPackages; + dockerImage = + let + pkgs = nixpkgsFor.${system}.native; + image = pkgs.callPackage ./docker.nix { + tag = pkgs.nix.version; + }; + in + pkgs.runCommand "docker-image-tarball-${pkgs.nix.version}" + { meta.description = "Docker image with Nix for ${system}"; } + '' + mkdir -p $out/nix-support + image=$out/image.tar.gz + ln -s ${image} $image + echo "file binary-dist $image" >> $out/nix-support/hydra-build-products + ''; + } + ); + + devShells = + let + makeShell = import ./packaging/dev-shell.nix { inherit lib devFlake; }; + prefixAttrs = prefix: lib.concatMapAttrs (k: v: { "${prefix}-${k}" = v; }); + in + forAllSystems ( + system: + prefixAttrs "native" ( + forAllStdenvs ( + stdenvName: + makeShell { + pkgs = nixpkgsFor.${system}.nativeForStdenv.${stdenvName}; + } + ) + ) + // lib.optionalAttrs (!nixpkgsFor.${system}.native.stdenv.isDarwin) ( + prefixAttrs "static" ( + forAllStdenvs ( + stdenvName: + makeShell { + pkgs = nixpkgsFor.${system}.nativeForStdenv.${stdenvName}.pkgsStatic; + } + ) + ) + // prefixAttrs "llvm" ( + forAllStdenvs ( + stdenvName: + makeShell { + pkgs = nixpkgsFor.${system}.nativeForStdenv.${stdenvName}.pkgsLLVM; + } + ) + ) + // prefixAttrs "cross" ( + forAllCrossSystems ( + crossSystem: + makeShell { + pkgs = nixpkgsFor.${system}.cross.${crossSystem}; + } + ) + ) + ) + // { + native = self.devShells.${system}.native-stdenv; + default = self.devShells.${system}.native; } ); - }; + }; } diff --git a/m4/gcc_bug_80431.m4 b/m4/gcc_bug_80431.m4 deleted file mode 100644 index cdc4ddb401a..00000000000 --- a/m4/gcc_bug_80431.m4 +++ /dev/null @@ -1,66 +0,0 @@ -# Ensure that this bug is not present in the C++ toolchain we are using. -# -# URL for bug: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=80431 -# -# The test program is from that issue, with only a slight modification -# to set an exit status instead of printing strings. -AC_DEFUN([ENSURE_NO_GCC_BUG_80431], -[ - AC_MSG_CHECKING([that GCC bug 80431 is fixed]) - AC_LANG_PUSH(C++) - AC_RUN_IFELSE( - [AC_LANG_PROGRAM( - [[ - #include - - static bool a = true; - static bool b = true; - - struct Options { }; - - struct Option - { - Option(Options * options) - { - a = false; - } - - ~Option() - { - b = false; - } - }; - - struct MyOptions : Options { }; - - struct MyOptions2 : virtual MyOptions - { - Option foo{this}; - }; - ]], - [[ - { - MyOptions2 opts; - } - return (a << 1) | b; - ]])], - [status_80431=0], - [status_80431=$?], - [status_80431='']) - AC_LANG_POP(C++) - AS_CASE([$status_80431], - [''],[ - AC_MSG_RESULT(cannot check because cross compiling) - AC_MSG_NOTICE(assume we are bug free) - ], - [0],[ - AC_MSG_RESULT(yes) - ], - [2],[ - AC_MSG_RESULT(no) - AC_MSG_ERROR(Cannot build Nix with C++ compiler with this bug) - ], - [ - AC_MSG_RESULT(unexpected result $status_80431: not expected failure with bug, ignoring) - ]) -]) diff --git a/maintainers/README.md b/maintainers/README.md index fd9bbed8e52..6553cd04815 100644 --- a/maintainers/README.md +++ b/maintainers/README.md @@ -37,7 +37,7 @@ The team is on Github as [@NixOS/nix-team](https://github.com/orgs/NixOS/teams/n The team meets twice a week (times are denoted in the [Europe/Amsterdam](https://en.m.wikipedia.org/wiki/Time_in_the_Netherlands) time zone): -- Discussion meeting: [Wednesday 21:00-22:00 Europe/Amsterdam](https://www.google.com/calendar/event?eid=ZG5rZzNyajRjajducGV2NGY5aGkzYWIwdnJfMjAyNDA1MDhUMTkwMDAwWiBiOW81MmZvYnFqYWs4b3E4bGZraGczdDBxZ0Bn) +- Discussion meeting: Wednesday 21:00-22:00 Europe/Amsterdam see [calendar](https://calendar.google.com/calendar/u/0/embed?src=b9o52fobqjak8oq8lfkhg3t0qg@group.calendar.google.com). 1. Triage issues and pull requests from the [No Status](#no-status) column (30 min) 2. Discuss issues and pull requests from the [To discuss](#to-discuss) column (30 min). @@ -46,7 +46,7 @@ The team meets twice a week (times are denoted in the [Europe/Amsterdam](https:/ - mark it as draft if it is blocked on the contributor - escalate it back to the team by moving it to To discuss, and leaving a comment as to why the issue needs to be discussed again. -- Work meeting: [Mondays 14:00-16:00 Europe/Amsterdam](https://www.google.com/calendar/event?eid=Ym52NDdzYnRic2NzcDcybjZiNDhpNzhpa3NfMjAyNDA1MTNUMTIwMDAwWiBiOW81MmZvYnFqYWs4b3E4bGZraGczdDBxZ0Bn) +- Work meeting: Mondays 14:00-16:00 Europe/Amsterdam see [calendar](https://calendar.google.com/calendar/u/0/embed?src=b9o52fobqjak8oq8lfkhg3t0qg@group.calendar.google.com). 1. Code review on pull requests from [In review](#in-review). 2. Other chores and tasks. diff --git a/maintainers/data/release-credits-email-to-handle.json b/maintainers/data/release-credits-email-to-handle.json index d7a238ebdd7..48e8685e6d9 100644 --- a/maintainers/data/release-credits-email-to-handle.json +++ b/maintainers/data/release-credits-email-to-handle.json @@ -98,5 +98,92 @@ "aks.kenji@protonmail.com": "a-kenji", "54070204+0x5a4@users.noreply.github.com": "0x5a4", "brian@bmcgee.ie": "brianmcgee", - "squalus@squalus.net": "squalus" + "squalus@squalus.net": "squalus", + "kusold@users.noreply.github.com": "kusold", + "37929162+mergify[bot]@users.noreply.github.com": "mergify[bot]", + "ilja@mailbox.org": "suruaku", + "and.ham95@gmail.com": "andrewhamon", + "andy.hamon@discordapp.com": "andrewhamon", + "siddarthkay@gmail.com": "siddarthkay", + "apoelstra@wpsoftware.net": "apoelstra", + "asmadeus@codewreck.org": "martinetd", + "tristan.ross@midstall.com": "RossComputerGuy", + "bryanlais@gmail.com": "bryango", + "157494086+allrealmsoflife@users.noreply.github.com": "allrealmsoflife", + "ConnorBaker01@gmail.com": "ConnorBaker", + "me@momee.mt": "momeemt", + "martin@push-f.com": "not-my-profile", + "90870942+trueNAHO@users.noreply.github.com": "trueNAHO", + "49885263+knotapun@users.noreply.github.com": "knotapun", + "iam@lach.pw": "CertainLach", + "elikowa@gmail.com": "elikoga", + "greg.curtis@jetpack.io": "gcurtis", + "git@sphalerite.org": "lheckemann", + "mightyiampresence@gmail.com": "mightyiam", + "spamfaenger@gmx.de": "dwt", + "graham@grahamc.com": "grahamc", + "wh0@users.noreply.github.com": "wh0", + "25388474+mupdt@users.noreply.github.com": "mupdt", + "anatoli@rainforce.org": "abitrolly", + "h0nIg@users.noreply.github.com": "h0nIg", + "CyberShadow@users.noreply.github.com": "CyberShadow", + "gavinnjohn@gmail.com": "Pandapip1", + "picnoir@alternativebit.fr": "picnoir", + "140354451+myclevorname@users.noreply.github.com": "myclevorname", + "bonniot@gmail.com": "dbdr", + "jack@wilsdon.me": "jackwilsdon", + "143541718+WxNzEMof@users.noreply.github.com": "the-sun-will-rise-tomorrow", + "fabianm88@gmail.com": "B4dM4n", + "silvan.mosberger@moduscreate.com": "infinisil", + "leandro.reina@ororatech.com": "kip93", + "else@someonex.net": "SomeoneSerge", + "aiden@aidenfoxivey.com": "aidenfoxivey", + "maxoscarhearnden@gmail.com": "MaxHearnden", + "silvanshade@users.noreply.github.com": "silvanshade", + "illia.bobyr@gmail.com": "ilya-bobyr", + "65963536+etherswangel@users.noreply.github.com": "stevalkr", + "thebenmachine+git@gmail.com": "bmillwood", + "leandro@kip93.net": "kip93", + "hello@briancamacho.me": "b-camacho", + "bcamacho@anduril.com": "bcamacho2", + "oldshensheep@gmail.com": "oldshensheep", + "thomasmiedema@gmail.com": "thomie", + "xokdvium@proton.me": "xokdvium", + "kaction@disroot.org": "KAction", + "serenity@kaction.cc": null, + "dev@erik.work": "Kirens", + "felix@alternativebit.fr": "picnoir", + "butirsky@gmail.com": "bam80", + "look@my.amazin.horse": "Valodim", + "jeremyfleischman@gmail.com": "jfly", + "vit.gottwald@gmail.com": "VitGottwald", + "a@unnamed.website": "anthowan", + "hello@whatsthecraic.net": "whatsthecraic", + "alex.rom23@mail.ru": "ajlekcahdp4", + "domagoj@tuta.com": "allrealmsoflife", + "uluc.sengil@gmail.com": "ulucs", + "prc.zhao@outlook.com": "Prince213", + "the-tumultuous-unicorn-of-darkness@gmx.com": "TheTumultuousUnicornOfDarkness", + "dev@rodney.id.au": "rvl", + "pe@pijul.org": "P-E-Meunier", + "yannik@floxdev.com": "ysndr", + "73017521+egorkonovalov@users.noreply.github.com": "egorkonovalov", + "raito@lix.systems": null, + "nikita.nikita.krasnov@gmail.com": "synalice", + "lucperkins@gmail.com": "lucperkins", + "vladimir.cunat@nic.cz": "vcunat", + "walther@technowledgy.de": "wolfgangwalther", + "jayesh.mail@gmail.com": "jayeshv", + "samuli.thomasson@pm.me": "SimSaladin", + "kevin@stravers.net": "kstrafe", + "poperigby@mailbox.org": "poperigby", + "cole.helbling@determinate.systems": "cole-h", + "donottellmetonottellyou@gmail.com": "donottellmetonottellyou", + "getchoo@tuta.io": "getchoo", + "alex.ford@determinate.systems": "gustavderdrache", + "stefan.r.boca@gmail.com": "stefanboca", + "gwenn.lebihan7@gmail.com": "gwennlbh", + "hey@ewen.works": "gwennlbh", + "matt@sturgeon.me.uk": "MattSturgeon", + "pbsds@hotmail.com": "pbsds" } \ No newline at end of file diff --git a/maintainers/data/release-credits-handle-to-name.json b/maintainers/data/release-credits-handle-to-name.json index 7657716688c..a6352c44b22 100644 --- a/maintainers/data/release-credits-handle-to-name.json +++ b/maintainers/data/release-credits-handle-to-name.json @@ -86,5 +86,81 @@ "Aleksanaa": "Aleksana", "YorikSar": "Yuriy Taraday", "kjeremy": "Jeremy Kolb", - "artemist": "Artemis Tosini" + "artemist": "Artemis Tosini", + "the-sun-will-rise-tomorrow": null, + "gcurtis": "Greg Curtis", + "ConnorBaker": "Connor Baker", + "abitrolly": "Anatoli Babenia", + "allrealmsoflife": "Domagoj Mi\u0161kovi\u0107", + "andrewhamon": "Andy Hamon", + "picnoir": "F\u00e9lix", + "dbdr": null, + "suruaku": "Ilja", + "jackwilsdon": "Jack Wilsdon", + "mergify[bot]": null, + "kusold": "Mike Kusold", + "lheckemann": "Linus Heckemann", + "h0nIg": null, + "grahamc": "Graham Christensen", + "not-my-profile": "Martin Fischer", + "CyberShadow": "Vladimir Panteleev", + "Pandapip1": "Gavin John", + "RossComputerGuy": "Tristan Ross", + "elikoga": null, + "martinetd": "Dominique Martinet", + "knotapun": "Parker Jones", + "mightyiam": "Shahar \"Dawn\" Or", + "siddarthkay": "Siddarth Kumar", + "apoelstra": "Andrew Poelstra", + "myclevorname": null, + "CertainLach": "Yaroslav Bolyukin", + "trueNAHO": "NAHO", + "wh0": null, + "mupdt": "Matej Urbas", + "momeemt": "Mutsuha Asada", + "dwt": "\u202erekc\u00e4H nitraM\u202e", + "aidenfoxivey": "Aiden Fox Ivey", + "ilya-bobyr": "Illia Bobyr", + "B4dM4n": "Fabian M\u00f6ller", + "silvanshade": null, + "bcamacho2": null, + "bmillwood": "Ben Millwood", + "stevalkr": "Steve Walker", + "SomeoneSerge": "Someone", + "b-camacho": "Brian Camacho", + "MaxHearnden": null, + "kip93": "Leandro Emmanuel Reina Kiperman", + "oldshensheep": "Ruby Rose", + "KAction": "Dmitry Bogatov", + "thomie": "Thomas Miedema", + "Kirens": "Erik Nygren", + "Prince213": "Sizhe Zhao", + "anthowan": "Anthony Wang", + "jfly": "Jeremy Fleischman", + "VitGottwald": "Vit Gottwald", + "bam80": "Andrey Butirsky", + "ulucs": null, + "P-E-Meunier": "Pierre-Etienne Meunier", + "ysndr": "Yannik Sander", + "TheTumultuousUnicornOfDarkness": "The Tumultuous Unicorn Of Darkness", + "ajlekcahdp4": "Alexander Romanov", + "Valodim": "Vincent Breitmoser", + "rvl": "Rodney Lorrimar", + "whatsthecraic": "Dean De Leo", + "gwennlbh": "Gwenn Le Bihan", + "donottellmetonottellyou": "Jade Masker", + "kstrafe": null, + "synalice": "Nikita Krasnov", + "poperigby": "PopeRigby", + "MattSturgeon": "Matt Sturgeon", + "lucperkins": "Luc Perkins", + "gustavderdrache": null, + "SimSaladin": "Samuli Thomasson", + "getchoo": "Seth Flynn", + "stefanboca": "Stefan Boca", + "wolfgangwalther": "Wolfgang Walther", + "pbsds": "Peder Bergebakken Sundt", + "egorkonovalov": "Egor Konovalov", + "jayeshv": "jayeshv", + "vcunat": "Vladim\u00edr \u010cun\u00e1t" } \ No newline at end of file diff --git a/maintainers/flake-module.nix b/maintainers/flake-module.nix index 1d4e85c8c5b..1058d633473 100644 --- a/maintainers/flake-module.nix +++ b/maintainers/flake-module.nix @@ -1,645 +1,811 @@ -{ lib, getSystem, inputs, ... }: +{ + lib, + getSystem, + inputs, + ... +}: { imports = [ inputs.git-hooks-nix.flakeModule ]; - perSystem = { config, pkgs, ... }: { + perSystem = + { config, pkgs, ... }: + { - # https://flake.parts/options/git-hooks-nix#options - pre-commit.settings = { - hooks = { - clang-format = { - enable = true; - excludes = [ - # We don't want to format test data - # ''tests/(?!nixos/).*\.nix'' - ''^src/[^/]*-tests/data/.*$'' + # https://flake.parts/options/git-hooks-nix#options + pre-commit.settings = { + hooks = { + # Conflicts are usually found by other checks, but not those in docs, + # and potentially other places. + check-merge-conflicts.enable = true; + # built-in check-merge-conflicts seems ineffective against those produced by mergify backports + check-merge-conflicts-2 = { + enable = true; + entry = "${pkgs.writeScript "check-merge-conflicts" '' + #!${pkgs.runtimeShell} + conflicts=false + for file in "$@"; do + if grep --with-filename --line-number -E '^>>>>>>> ' -- "$file"; then + conflicts=true + fi + done + if $conflicts; then + echo "ERROR: found merge/patch conflicts in files" + exit 1 + fi + ''}"; + }; + meson-format = { + enable = true; + files = "(meson.build|meson.options)$"; + entry = "${pkgs.writeScript "format-meson" '' + #!${pkgs.runtimeShell} + for file in "$@"; do + ${lib.getExe pkgs.meson} format -ic ${../meson.format} "$file" + done + ''}"; + excludes = [ + # We haven't applied formatting to these files yet + ''^doc/manual/meson.build$'' + ''^doc/manual/source/command-ref/meson.build$'' + ''^doc/manual/source/development/meson.build$'' + ''^doc/manual/source/language/meson.build$'' + ''^doc/manual/source/meson.build$'' + ''^doc/manual/source/release-notes/meson.build$'' + ''^doc/manual/source/store/meson.build$'' + ''^misc/bash/meson.build$'' + ''^misc/fish/meson.build$'' + ''^misc/launchd/meson.build$'' + ''^misc/meson.build$'' + ''^misc/systemd/meson.build$'' + ''^misc/zsh/meson.build$'' + ''^nix-meson-build-support/$'' + ''^nix-meson-build-support/big-objs/meson.build$'' + ''^nix-meson-build-support/common/meson.build$'' + ''^nix-meson-build-support/deps-lists/meson.build$'' + ''^nix-meson-build-support/export/meson.build$'' + ''^nix-meson-build-support/export-all-symbols/meson.build$'' + ''^nix-meson-build-support/generate-header/meson.build$'' + ''^nix-meson-build-support/libatomic/meson.build$'' + ''^nix-meson-build-support/subprojects/meson.build$'' + ''^scripts/meson.build$'' + ''^src/external-api-docs/meson.build$'' + ''^src/internal-api-docs/meson.build$'' + ''^src/libcmd/include/nix/cmd/meson.build$'' + ''^src/libcmd/meson.build$'' + ''^src/libcmd/nix-meson-build-support$'' + ''^src/libexpr/include/nix/expr/meson.build$'' + ''^src/libexpr/meson.build$'' + ''^src/libexpr/nix-meson-build-support$'' + ''^src/libexpr-c/meson.build$'' + ''^src/libexpr-c/nix-meson-build-support$'' + ''^src/libexpr-test-support/meson.build$'' + ''^src/libexpr-test-support/nix-meson-build-support$'' + ''^src/libexpr-tests/meson.build$'' + ''^src/libexpr-tests/nix-meson-build-support$'' + ''^src/libfetchers/include/nix/fetchers/meson.build$'' + ''^src/libfetchers/meson.build$'' + ''^src/libfetchers/nix-meson-build-support$'' + ''^src/libfetchers-c/meson.build$'' + ''^src/libfetchers-c/nix-meson-build-support$'' + ''^src/libfetchers-tests/meson.build$'' + ''^src/libfetchers-tests/nix-meson-build-support$'' + ''^src/libflake/include/nix/flake/meson.build$'' + ''^src/libflake/meson.build$'' + ''^src/libflake/nix-meson-build-support$'' + ''^src/libflake-c/meson.build$'' + ''^src/libflake-c/nix-meson-build-support$'' + ''^src/libflake-tests/meson.build$'' + ''^src/libflake-tests/nix-meson-build-support$'' + ''^src/libmain/include/nix/main/meson.build$'' + ''^src/libmain/meson.build$'' + ''^src/libmain/nix-meson-build-support$'' + ''^src/libmain-c/meson.build$'' + ''^src/libmain-c/nix-meson-build-support$'' + ''^src/libstore/include/nix/store/meson.build$'' + ''^src/libstore/meson.build$'' + ''^src/libstore/nix-meson-build-support$'' + ''^src/libstore/unix/include/nix/store/meson.build$'' + ''^src/libstore/unix/meson.build$'' + ''^src/libstore/windows/meson.build$'' + ''^src/libstore-c/meson.build$'' + ''^src/libstore-c/nix-meson-build-support$'' + ''^src/libstore-test-support/include/nix/store/tests/meson.build$'' + ''^src/libstore-test-support/meson.build$'' + ''^src/libstore-test-support/nix-meson-build-support$'' + ''^src/libstore-tests/meson.build$'' + ''^src/libstore-tests/nix-meson-build-support$'' + ''^src/libutil/meson.build$'' + ''^src/libutil/nix-meson-build-support$'' + ''^src/libutil/unix/include/nix/util/meson.build$'' + ''^src/libutil/unix/meson.build$'' + ''^src/libutil/windows/meson.build$'' + ''^src/libutil-c/meson.build$'' + ''^src/libutil-c/nix-meson-build-support$'' + ''^src/libutil-test-support/include/nix/util/tests/meson.build$'' + ''^src/libutil-test-support/meson.build$'' + ''^src/libutil-test-support/nix-meson-build-support$'' + ''^src/libutil-tests/meson.build$'' + ''^src/libutil-tests/nix-meson-build-support$'' + ''^src/nix/meson.build$'' + ''^src/nix/nix-meson-build-support$'' + ''^src/perl/lib/Nix/meson.build$'' + ''^src/perl/meson.build$'' + ''^tests/functional/ca/meson.build$'' + ''^tests/functional/common/meson.build$'' + ''^tests/functional/dyn-drv/meson.build$'' + ''^tests/functional/flakes/meson.build$'' + ''^tests/functional/git-hashing/meson.build$'' + ''^tests/functional/local-overlay-store/meson.build$'' + ''^tests/functional/meson.build$'' + ''^src/libcmd/meson.options$'' + ''^src/libexpr/meson.options$'' + ''^src/libstore/meson.options$'' + ''^src/libutil/meson.options$'' + ''^src/libutil-c/meson.options$'' + ''^src/nix/meson.options$'' + ''^src/perl/meson.options$'' + ]; + }; + nixfmt-rfc-style = { + enable = true; + excludes = [ + # Invalid + ''^tests/functional/lang/parse-.*\.nix$'' - # Don't format vendored code - ''^doc/manual/redirects\.js$'' - ''^doc/manual/theme/highlight\.js$'' + # Formatting-sensitive + ''^tests/functional/lang/eval-okay-curpos\.nix$'' + ''^tests/functional/lang/.*comment.*\.nix$'' + ''^tests/functional/lang/.*newline.*\.nix$'' + ''^tests/functional/lang/.*eol.*\.nix$'' - # We haven't applied formatting to these files yet - ''^doc/manual/redirects\.js$'' - ''^doc/manual/theme/highlight\.js$'' - ''^precompiled-headers\.h$'' - ''^src/build-remote/build-remote\.cc$'' - ''^src/libcmd/built-path\.cc$'' - ''^src/libcmd/built-path\.hh$'' - ''^src/libcmd/common-eval-args\.cc$'' - ''^src/libcmd/common-eval-args\.hh$'' - ''^src/libcmd/editor-for\.cc$'' - ''^src/libcmd/installable-attr-path\.cc$'' - ''^src/libcmd/installable-attr-path\.hh$'' - ''^src/libcmd/installable-derived-path\.cc$'' - ''^src/libcmd/installable-derived-path\.hh$'' - ''^src/libcmd/installable-flake\.cc$'' - ''^src/libcmd/installable-flake\.hh$'' - ''^src/libcmd/installable-value\.cc$'' - ''^src/libcmd/installable-value\.hh$'' - ''^src/libcmd/installables\.cc$'' - ''^src/libcmd/installables\.hh$'' - ''^src/libcmd/legacy\.hh$'' - ''^src/libcmd/markdown\.cc$'' - ''^src/libcmd/misc-store-flags\.cc$'' - ''^src/libcmd/repl-interacter\.cc$'' - ''^src/libcmd/repl-interacter\.hh$'' - ''^src/libcmd/repl\.cc$'' - ''^src/libcmd/repl\.hh$'' - ''^src/libexpr-c/nix_api_expr\.cc$'' - ''^src/libexpr-c/nix_api_external\.cc$'' - ''^src/libexpr/attr-path\.cc$'' - ''^src/libexpr/attr-path\.hh$'' - ''^src/libexpr/attr-set\.cc$'' - ''^src/libexpr/attr-set\.hh$'' - ''^src/libexpr/eval-cache\.cc$'' - ''^src/libexpr/eval-cache\.hh$'' - ''^src/libexpr/eval-error\.cc$'' - ''^src/libexpr/eval-inline\.hh$'' - ''^src/libexpr/eval-settings\.cc$'' - ''^src/libexpr/eval-settings\.hh$'' - ''^src/libexpr/eval\.cc$'' - ''^src/libexpr/eval\.hh$'' - ''^src/libexpr/function-trace\.cc$'' - ''^src/libexpr/gc-small-vector\.hh$'' - ''^src/libexpr/get-drvs\.cc$'' - ''^src/libexpr/get-drvs\.hh$'' - ''^src/libexpr/json-to-value\.cc$'' - ''^src/libexpr/nixexpr\.cc$'' - ''^src/libexpr/nixexpr\.hh$'' - ''^src/libexpr/parser-state\.hh$'' - ''^src/libexpr/pos-table\.hh$'' - ''^src/libexpr/primops\.cc$'' - ''^src/libexpr/primops\.hh$'' - ''^src/libexpr/primops/context\.cc$'' - ''^src/libexpr/primops/fetchClosure\.cc$'' - ''^src/libexpr/primops/fetchMercurial\.cc$'' - ''^src/libexpr/primops/fetchTree\.cc$'' - ''^src/libexpr/primops/fromTOML\.cc$'' - ''^src/libexpr/print-ambiguous\.cc$'' - ''^src/libexpr/print-ambiguous\.hh$'' - ''^src/libexpr/print-options\.hh$'' - ''^src/libexpr/print\.cc$'' - ''^src/libexpr/print\.hh$'' - ''^src/libexpr/search-path\.cc$'' - ''^src/libexpr/symbol-table\.hh$'' - ''^src/libexpr/value-to-json\.cc$'' - ''^src/libexpr/value-to-json\.hh$'' - ''^src/libexpr/value-to-xml\.cc$'' - ''^src/libexpr/value-to-xml\.hh$'' - ''^src/libexpr/value\.hh$'' - ''^src/libexpr/value/context\.cc$'' - ''^src/libexpr/value/context\.hh$'' - ''^src/libfetchers/attrs\.cc$'' - ''^src/libfetchers/cache\.cc$'' - ''^src/libfetchers/cache\.hh$'' - ''^src/libfetchers/fetch-settings\.cc$'' - ''^src/libfetchers/fetch-settings\.hh$'' - ''^src/libfetchers/fetch-to-store\.cc$'' - ''^src/libfetchers/fetchers\.cc$'' - ''^src/libfetchers/fetchers\.hh$'' - ''^src/libfetchers/filtering-source-accessor\.cc$'' - ''^src/libfetchers/filtering-source-accessor\.hh$'' - ''^src/libfetchers/fs-source-accessor\.cc$'' - ''^src/libfetchers/fs-source-accessor\.hh$'' - ''^src/libfetchers/git-utils\.cc$'' - ''^src/libfetchers/git-utils\.hh$'' - ''^src/libfetchers/github\.cc$'' - ''^src/libfetchers/indirect\.cc$'' - ''^src/libfetchers/memory-source-accessor\.cc$'' - ''^src/libfetchers/path\.cc$'' - ''^src/libfetchers/registry\.cc$'' - ''^src/libfetchers/registry\.hh$'' - ''^src/libfetchers/tarball\.cc$'' - ''^src/libfetchers/tarball\.hh$'' - ''^src/libfetchers/git\.cc$'' - ''^src/libfetchers/mercurial\.cc$'' - ''^src/libflake/flake/config\.cc$'' - ''^src/libflake/flake/flake\.cc$'' - ''^src/libflake/flake/flake\.hh$'' - ''^src/libflake/flake/flakeref\.cc$'' - ''^src/libflake/flake/flakeref\.hh$'' - ''^src/libflake/flake/lockfile\.cc$'' - ''^src/libflake/flake/lockfile\.hh$'' - ''^src/libflake/flake/url-name\.cc$'' - ''^src/libmain/common-args\.cc$'' - ''^src/libmain/common-args\.hh$'' - ''^src/libmain/loggers\.cc$'' - ''^src/libmain/loggers\.hh$'' - ''^src/libmain/progress-bar\.cc$'' - ''^src/libmain/shared\.cc$'' - ''^src/libmain/shared\.hh$'' - ''^src/libmain/unix/stack\.cc$'' - ''^src/libstore/binary-cache-store\.cc$'' - ''^src/libstore/binary-cache-store\.hh$'' - ''^src/libstore/build-result\.hh$'' - ''^src/libstore/builtins\.hh$'' - ''^src/libstore/builtins/buildenv\.cc$'' - ''^src/libstore/builtins/buildenv\.hh$'' - ''^src/libstore/common-protocol-impl\.hh$'' - ''^src/libstore/common-protocol\.cc$'' - ''^src/libstore/common-protocol\.hh$'' - ''^src/libstore/common-ssh-store-config\.hh$'' - ''^src/libstore/content-address\.cc$'' - ''^src/libstore/content-address\.hh$'' - ''^src/libstore/daemon\.cc$'' - ''^src/libstore/daemon\.hh$'' - ''^src/libstore/derivations\.cc$'' - ''^src/libstore/derivations\.hh$'' - ''^src/libstore/derived-path-map\.cc$'' - ''^src/libstore/derived-path-map\.hh$'' - ''^src/libstore/derived-path\.cc$'' - ''^src/libstore/derived-path\.hh$'' - ''^src/libstore/downstream-placeholder\.cc$'' - ''^src/libstore/downstream-placeholder\.hh$'' - ''^src/libstore/dummy-store\.cc$'' - ''^src/libstore/export-import\.cc$'' - ''^src/libstore/filetransfer\.cc$'' - ''^src/libstore/filetransfer\.hh$'' - ''^src/libstore/gc-store\.hh$'' - ''^src/libstore/globals\.cc$'' - ''^src/libstore/globals\.hh$'' - ''^src/libstore/http-binary-cache-store\.cc$'' - ''^src/libstore/legacy-ssh-store\.cc$'' - ''^src/libstore/legacy-ssh-store\.hh$'' - ''^src/libstore/length-prefixed-protocol-helper\.hh$'' - ''^src/libstore/linux/personality\.cc$'' - ''^src/libstore/linux/personality\.hh$'' - ''^src/libstore/local-binary-cache-store\.cc$'' - ''^src/libstore/local-fs-store\.cc$'' - ''^src/libstore/local-fs-store\.hh$'' - ''^src/libstore/log-store\.cc$'' - ''^src/libstore/log-store\.hh$'' - ''^src/libstore/machines\.cc$'' - ''^src/libstore/machines\.hh$'' - ''^src/libstore/make-content-addressed\.cc$'' - ''^src/libstore/make-content-addressed\.hh$'' - ''^src/libstore/misc\.cc$'' - ''^src/libstore/names\.cc$'' - ''^src/libstore/names\.hh$'' - ''^src/libstore/nar-accessor\.cc$'' - ''^src/libstore/nar-accessor\.hh$'' - ''^src/libstore/nar-info-disk-cache\.cc$'' - ''^src/libstore/nar-info-disk-cache\.hh$'' - ''^src/libstore/nar-info\.cc$'' - ''^src/libstore/nar-info\.hh$'' - ''^src/libstore/outputs-spec\.cc$'' - ''^src/libstore/outputs-spec\.hh$'' - ''^src/libstore/parsed-derivations\.cc$'' - ''^src/libstore/path-info\.cc$'' - ''^src/libstore/path-info\.hh$'' - ''^src/libstore/path-references\.cc$'' - ''^src/libstore/path-regex\.hh$'' - ''^src/libstore/path-with-outputs\.cc$'' - ''^src/libstore/path\.cc$'' - ''^src/libstore/path\.hh$'' - ''^src/libstore/pathlocks\.cc$'' - ''^src/libstore/pathlocks\.hh$'' - ''^src/libstore/profiles\.cc$'' - ''^src/libstore/profiles\.hh$'' - ''^src/libstore/realisation\.cc$'' - ''^src/libstore/realisation\.hh$'' - ''^src/libstore/remote-fs-accessor\.cc$'' - ''^src/libstore/remote-fs-accessor\.hh$'' - ''^src/libstore/remote-store-connection\.hh$'' - ''^src/libstore/remote-store\.cc$'' - ''^src/libstore/remote-store\.hh$'' - ''^src/libstore/s3-binary-cache-store\.cc$'' - ''^src/libstore/s3\.hh$'' - ''^src/libstore/serve-protocol-impl\.cc$'' - ''^src/libstore/serve-protocol-impl\.hh$'' - ''^src/libstore/serve-protocol\.cc$'' - ''^src/libstore/serve-protocol\.hh$'' - ''^src/libstore/sqlite\.cc$'' - ''^src/libstore/sqlite\.hh$'' - ''^src/libstore/ssh-store\.cc$'' - ''^src/libstore/ssh\.cc$'' - ''^src/libstore/ssh\.hh$'' - ''^src/libstore/store-api\.cc$'' - ''^src/libstore/store-api\.hh$'' - ''^src/libstore/store-dir-config\.hh$'' - ''^src/libstore/build/derivation-goal\.cc$'' - ''^src/libstore/build/derivation-goal\.hh$'' - ''^src/libstore/build/drv-output-substitution-goal\.cc$'' - ''^src/libstore/build/drv-output-substitution-goal\.hh$'' - ''^src/libstore/build/entry-points\.cc$'' - ''^src/libstore/build/goal\.cc$'' - ''^src/libstore/build/goal\.hh$'' - ''^src/libstore/unix/build/hook-instance\.cc$'' - ''^src/libstore/unix/build/local-derivation-goal\.cc$'' - ''^src/libstore/unix/build/local-derivation-goal\.hh$'' - ''^src/libstore/build/substitution-goal\.cc$'' - ''^src/libstore/build/substitution-goal\.hh$'' - ''^src/libstore/build/worker\.cc$'' - ''^src/libstore/build/worker\.hh$'' - ''^src/libstore/builtins/fetchurl\.cc$'' - ''^src/libstore/builtins/unpack-channel\.cc$'' - ''^src/libstore/gc\.cc$'' - ''^src/libstore/local-overlay-store\.cc$'' - ''^src/libstore/local-overlay-store\.hh$'' - ''^src/libstore/local-store\.cc$'' - ''^src/libstore/local-store\.hh$'' - ''^src/libstore/unix/user-lock\.cc$'' - ''^src/libstore/unix/user-lock\.hh$'' - ''^src/libstore/optimise-store\.cc$'' - ''^src/libstore/unix/pathlocks\.cc$'' - ''^src/libstore/posix-fs-canonicalise\.cc$'' - ''^src/libstore/posix-fs-canonicalise\.hh$'' - ''^src/libstore/uds-remote-store\.cc$'' - ''^src/libstore/uds-remote-store\.hh$'' - ''^src/libstore/windows/build\.cc$'' - ''^src/libstore/worker-protocol-impl\.hh$'' - ''^src/libstore/worker-protocol\.cc$'' - ''^src/libstore/worker-protocol\.hh$'' - ''^src/libutil-c/nix_api_util_internal\.h$'' - ''^src/libutil/archive\.cc$'' - ''^src/libutil/archive\.hh$'' - ''^src/libutil/args\.cc$'' - ''^src/libutil/args\.hh$'' - ''^src/libutil/args/root\.hh$'' - ''^src/libutil/callback\.hh$'' - ''^src/libutil/canon-path\.cc$'' - ''^src/libutil/canon-path\.hh$'' - ''^src/libutil/chunked-vector\.hh$'' - ''^src/libutil/closure\.hh$'' - ''^src/libutil/comparator\.hh$'' - ''^src/libutil/compute-levels\.cc$'' - ''^src/libutil/config-impl\.hh$'' - ''^src/libutil/config\.cc$'' - ''^src/libutil/config\.hh$'' - ''^src/libutil/current-process\.cc$'' - ''^src/libutil/current-process\.hh$'' - ''^src/libutil/english\.cc$'' - ''^src/libutil/english\.hh$'' - ''^src/libutil/error\.cc$'' - ''^src/libutil/error\.hh$'' - ''^src/libutil/exit\.hh$'' - ''^src/libutil/experimental-features\.cc$'' - ''^src/libutil/experimental-features\.hh$'' - ''^src/libutil/file-content-address\.cc$'' - ''^src/libutil/file-content-address\.hh$'' - ''^src/libutil/file-descriptor\.cc$'' - ''^src/libutil/file-descriptor\.hh$'' - ''^src/libutil/file-path-impl\.hh$'' - ''^src/libutil/file-path\.hh$'' - ''^src/libutil/file-system\.cc$'' - ''^src/libutil/file-system\.hh$'' - ''^src/libutil/finally\.hh$'' - ''^src/libutil/fmt\.hh$'' - ''^src/libutil/fs-sink\.cc$'' - ''^src/libutil/fs-sink\.hh$'' - ''^src/libutil/git\.cc$'' - ''^src/libutil/git\.hh$'' - ''^src/libutil/hash\.cc$'' - ''^src/libutil/hash\.hh$'' - ''^src/libutil/hilite\.cc$'' - ''^src/libutil/hilite\.hh$'' - ''^src/libutil/source-accessor\.hh$'' - ''^src/libutil/json-impls\.hh$'' - ''^src/libutil/json-utils\.cc$'' - ''^src/libutil/json-utils\.hh$'' - ''^src/libutil/linux/cgroup\.cc$'' - ''^src/libutil/linux/namespaces\.cc$'' - ''^src/libutil/logging\.cc$'' - ''^src/libutil/logging\.hh$'' - ''^src/libutil/lru-cache\.hh$'' - ''^src/libutil/memory-source-accessor\.cc$'' - ''^src/libutil/memory-source-accessor\.hh$'' - ''^src/libutil/pool\.hh$'' - ''^src/libutil/position\.cc$'' - ''^src/libutil/position\.hh$'' - ''^src/libutil/posix-source-accessor\.cc$'' - ''^src/libutil/posix-source-accessor\.hh$'' - ''^src/libutil/processes\.hh$'' - ''^src/libutil/ref\.hh$'' - ''^src/libutil/references\.cc$'' - ''^src/libutil/references\.hh$'' - ''^src/libutil/regex-combinators\.hh$'' - ''^src/libutil/serialise\.cc$'' - ''^src/libutil/serialise\.hh$'' - ''^src/libutil/signals\.hh$'' - ''^src/libutil/signature/local-keys\.cc$'' - ''^src/libutil/signature/local-keys\.hh$'' - ''^src/libutil/signature/signer\.cc$'' - ''^src/libutil/signature/signer\.hh$'' - ''^src/libutil/source-accessor\.cc$'' - ''^src/libutil/source-accessor\.hh$'' - ''^src/libutil/source-path\.cc$'' - ''^src/libutil/source-path\.hh$'' - ''^src/libutil/split\.hh$'' - ''^src/libutil/suggestions\.cc$'' - ''^src/libutil/suggestions\.hh$'' - ''^src/libutil/sync\.hh$'' - ''^src/libutil/terminal\.cc$'' - ''^src/libutil/terminal\.hh$'' - ''^src/libutil/thread-pool\.cc$'' - ''^src/libutil/thread-pool\.hh$'' - ''^src/libutil/topo-sort\.hh$'' - ''^src/libutil/types\.hh$'' - ''^src/libutil/unix/file-descriptor\.cc$'' - ''^src/libutil/unix/file-path\.cc$'' - ''^src/libutil/unix/monitor-fd\.hh$'' - ''^src/libutil/unix/processes\.cc$'' - ''^src/libutil/unix/signals-impl\.hh$'' - ''^src/libutil/unix/signals\.cc$'' - ''^src/libutil/unix-domain-socket\.cc$'' - ''^src/libutil/unix/users\.cc$'' - ''^src/libutil/url-parts\.hh$'' - ''^src/libutil/url\.cc$'' - ''^src/libutil/url\.hh$'' - ''^src/libutil/users\.cc$'' - ''^src/libutil/users\.hh$'' - ''^src/libutil/util\.cc$'' - ''^src/libutil/util\.hh$'' - ''^src/libutil/variant-wrapper\.hh$'' - ''^src/libutil/windows/file-descriptor\.cc$'' - ''^src/libutil/windows/file-path\.cc$'' - ''^src/libutil/windows/processes\.cc$'' - ''^src/libutil/windows/users\.cc$'' - ''^src/libutil/windows/windows-error\.cc$'' - ''^src/libutil/windows/windows-error\.hh$'' - ''^src/libutil/xml-writer\.cc$'' - ''^src/libutil/xml-writer\.hh$'' - ''^src/nix-build/nix-build\.cc$'' - ''^src/nix-channel/nix-channel\.cc$'' - ''^src/nix-collect-garbage/nix-collect-garbage\.cc$'' - ''^src/nix-env/buildenv.nix$'' - ''^src/nix-env/nix-env\.cc$'' - ''^src/nix-env/user-env\.cc$'' - ''^src/nix-env/user-env\.hh$'' - ''^src/nix-instantiate/nix-instantiate\.cc$'' - ''^src/nix-store/dotgraph\.cc$'' - ''^src/nix-store/graphml\.cc$'' - ''^src/nix-store/nix-store\.cc$'' - ''^src/nix/add-to-store\.cc$'' - ''^src/nix/app\.cc$'' - ''^src/nix/build\.cc$'' - ''^src/nix/bundle\.cc$'' - ''^src/nix/cat\.cc$'' - ''^src/nix/config-check\.cc$'' - ''^src/nix/config\.cc$'' - ''^src/nix/copy\.cc$'' - ''^src/nix/derivation-add\.cc$'' - ''^src/nix/derivation-show\.cc$'' - ''^src/nix/derivation\.cc$'' - ''^src/nix/develop\.cc$'' - ''^src/nix/diff-closures\.cc$'' - ''^src/nix/dump-path\.cc$'' - ''^src/nix/edit\.cc$'' - ''^src/nix/eval\.cc$'' - ''^src/nix/flake\.cc$'' - ''^src/nix/fmt\.cc$'' - ''^src/nix/hash\.cc$'' - ''^src/nix/log\.cc$'' - ''^src/nix/ls\.cc$'' - ''^src/nix/main\.cc$'' - ''^src/nix/make-content-addressed\.cc$'' - ''^src/nix/nar\.cc$'' - ''^src/nix/optimise-store\.cc$'' - ''^src/nix/path-from-hash-part\.cc$'' - ''^src/nix/path-info\.cc$'' - ''^src/nix/prefetch\.cc$'' - ''^src/nix/profile\.cc$'' - ''^src/nix/realisation\.cc$'' - ''^src/nix/registry\.cc$'' - ''^src/nix/repl\.cc$'' - ''^src/nix/run\.cc$'' - ''^src/nix/run\.hh$'' - ''^src/nix/search\.cc$'' - ''^src/nix/sigs\.cc$'' - ''^src/nix/store-copy-log\.cc$'' - ''^src/nix/store-delete\.cc$'' - ''^src/nix/store-gc\.cc$'' - ''^src/nix/store-info\.cc$'' - ''^src/nix/store-repair\.cc$'' - ''^src/nix/store\.cc$'' - ''^src/nix/unix/daemon\.cc$'' - ''^src/nix/upgrade-nix\.cc$'' - ''^src/nix/verify\.cc$'' - ''^src/nix/why-depends\.cc$'' + # Syntax tests + ''^tests/functional/shell.shebang\.nix$'' + ''^tests/functional/lang/eval-okay-ind-string\.nix$'' - ''^tests/functional/plugins/plugintest\.cc'' - ''^tests/functional/test-libstoreconsumer/main\.cc'' - ''^tests/nixos/ca-fd-leak/sender\.c'' - ''^tests/nixos/ca-fd-leak/smuggler\.c'' - ''^tests/nixos/user-sandboxing/attacker\.c'' - ''^src/libexpr-test-support/tests/libexpr\.hh'' - ''^src/libexpr-test-support/tests/value/context\.cc'' - ''^src/libexpr-test-support/tests/value/context\.hh'' - ''^src/libexpr-tests/derived-path\.cc'' - ''^src/libexpr-tests/error_traces\.cc'' - ''^src/libexpr-tests/eval\.cc'' - ''^src/libexpr-tests/json\.cc'' - ''^src/libexpr-tests/main\.cc'' - ''^src/libexpr-tests/primops\.cc'' - ''^src/libexpr-tests/search-path\.cc'' - ''^src/libexpr-tests/trivial\.cc'' - ''^src/libexpr-tests/value/context\.cc'' - ''^src/libexpr-tests/value/print\.cc'' - ''^src/libfetchers-tests/public-key\.cc'' - ''^src/libflake-tests/flakeref\.cc'' - ''^src/libflake-tests/url-name\.cc'' - ''^src/libstore-test-support/tests/derived-path\.cc'' - ''^src/libstore-test-support/tests/derived-path\.hh'' - ''^src/libstore-test-support/tests/nix_api_store\.hh'' - ''^src/libstore-test-support/tests/outputs-spec\.cc'' - ''^src/libstore-test-support/tests/outputs-spec\.hh'' - ''^src/libstore-test-support/tests/path\.cc'' - ''^src/libstore-test-support/tests/path\.hh'' - ''^src/libstore-test-support/tests/protocol\.hh'' - ''^src/libstore-tests/common-protocol\.cc'' - ''^src/libstore-tests/content-address\.cc'' - ''^src/libstore-tests/derivation\.cc'' - ''^src/libstore-tests/derived-path\.cc'' - ''^src/libstore-tests/downstream-placeholder\.cc'' - ''^src/libstore-tests/machines\.cc'' - ''^src/libstore-tests/nar-info-disk-cache\.cc'' - ''^src/libstore-tests/nar-info\.cc'' - ''^src/libstore-tests/outputs-spec\.cc'' - ''^src/libstore-tests/path-info\.cc'' - ''^src/libstore-tests/path\.cc'' - ''^src/libstore-tests/serve-protocol\.cc'' - ''^src/libstore-tests/worker-protocol\.cc'' - ''^src/libutil-test-support/tests/characterization\.hh'' - ''^src/libutil-test-support/tests/hash\.cc'' - ''^src/libutil-test-support/tests/hash\.hh'' - ''^src/libutil-tests/args\.cc'' - ''^src/libutil-tests/canon-path\.cc'' - ''^src/libutil-tests/chunked-vector\.cc'' - ''^src/libutil-tests/closure\.cc'' - ''^src/libutil-tests/compression\.cc'' - ''^src/libutil-tests/config\.cc'' - ''^src/libutil-tests/file-content-address\.cc'' - ''^src/libutil-tests/git\.cc'' - ''^src/libutil-tests/hash\.cc'' - ''^src/libutil-tests/hilite\.cc'' - ''^src/libutil-tests/json-utils\.cc'' - ''^src/libutil-tests/logging\.cc'' - ''^src/libutil-tests/lru-cache\.cc'' - ''^src/libutil-tests/pool\.cc'' - ''^src/libutil-tests/references\.cc'' - ''^src/libutil-tests/suggestions\.cc'' - ''^src/libutil-tests/url\.cc'' - ''^src/libutil-tests/xml-writer\.cc'' - ]; - }; - shellcheck = { - enable = true; - excludes = [ - # We haven't linted these files yet - ''^config/install-sh$'' - ''^misc/bash/completion\.sh$'' - ''^misc/fish/completion\.fish$'' - ''^misc/zsh/completion\.zsh$'' - ''^scripts/create-darwin-volume\.sh$'' - ''^scripts/install-darwin-multi-user\.sh$'' - ''^scripts/install-multi-user\.sh$'' - ''^scripts/install-systemd-multi-user\.sh$'' - ''^src/nix/get-env\.sh$'' - ''^tests/functional/ca/build-dry\.sh$'' - ''^tests/functional/ca/build-with-garbage-path\.sh$'' - ''^tests/functional/ca/common\.sh$'' - ''^tests/functional/ca/concurrent-builds\.sh$'' - ''^tests/functional/ca/eval-store\.sh$'' - ''^tests/functional/ca/gc\.sh$'' - ''^tests/functional/ca/import-from-derivation\.sh$'' - ''^tests/functional/ca/new-build-cmd\.sh$'' - ''^tests/functional/ca/nix-shell\.sh$'' - ''^tests/functional/ca/post-hook\.sh$'' - ''^tests/functional/ca/recursive\.sh$'' - ''^tests/functional/ca/repl\.sh$'' - ''^tests/functional/ca/selfref-gc\.sh$'' - ''^tests/functional/ca/why-depends\.sh$'' - ''^tests/functional/characterisation-test-infra\.sh$'' - ''^tests/functional/common/vars-and-functions\.sh$'' - ''^tests/functional/completions\.sh$'' - ''^tests/functional/compute-levels\.sh$'' - ''^tests/functional/config\.sh$'' - ''^tests/functional/db-migration\.sh$'' - ''^tests/functional/debugger\.sh$'' - ''^tests/functional/dependencies\.builder0\.sh$'' - ''^tests/functional/dependencies\.sh$'' - ''^tests/functional/dump-db\.sh$'' - ''^tests/functional/dyn-drv/build-built-drv\.sh$'' - ''^tests/functional/dyn-drv/common\.sh$'' - ''^tests/functional/dyn-drv/dep-built-drv\.sh$'' - ''^tests/functional/dyn-drv/eval-outputOf\.sh$'' - ''^tests/functional/dyn-drv/old-daemon-error-hack\.sh$'' - ''^tests/functional/dyn-drv/recursive-mod-json\.sh$'' - ''^tests/functional/eval-store\.sh$'' - ''^tests/functional/export-graph\.sh$'' - ''^tests/functional/export\.sh$'' - ''^tests/functional/extra-sandbox-profile\.sh$'' - ''^tests/functional/fetchClosure\.sh$'' - ''^tests/functional/fetchGit\.sh$'' - ''^tests/functional/fetchGitRefs\.sh$'' - ''^tests/functional/fetchGitSubmodules\.sh$'' - ''^tests/functional/fetchGitVerification\.sh$'' - ''^tests/functional/fetchMercurial\.sh$'' - ''^tests/functional/fixed\.builder1\.sh$'' - ''^tests/functional/fixed\.builder2\.sh$'' - ''^tests/functional/fixed\.sh$'' - ''^tests/functional/flakes/absolute-paths\.sh$'' - ''^tests/functional/flakes/check\.sh$'' - ''^tests/functional/flakes/config\.sh$'' - ''^tests/functional/flakes/flakes\.sh$'' - ''^tests/functional/flakes/follow-paths\.sh$'' - ''^tests/functional/flakes/prefetch\.sh$'' - ''^tests/functional/flakes/run\.sh$'' - ''^tests/functional/flakes/show\.sh$'' - ''^tests/functional/fmt\.sh$'' - ''^tests/functional/fmt\.simple\.sh$'' - ''^tests/functional/gc-auto\.sh$'' - ''^tests/functional/gc-concurrent\.builder\.sh$'' - ''^tests/functional/gc-concurrent\.sh$'' - ''^tests/functional/gc-concurrent2\.builder\.sh$'' - ''^tests/functional/gc-non-blocking\.sh$'' - ''^tests/functional/git-hashing/common\.sh$'' - ''^tests/functional/git-hashing/simple\.sh$'' - ''^tests/functional/hash-convert\.sh$'' - ''^tests/functional/impure-derivations\.sh$'' - ''^tests/functional/impure-eval\.sh$'' - ''^tests/functional/install-darwin\.sh$'' - ''^tests/functional/legacy-ssh-store\.sh$'' - ''^tests/functional/linux-sandbox\.sh$'' - ''^tests/functional/local-overlay-store/add-lower-inner\.sh$'' - ''^tests/functional/local-overlay-store/add-lower\.sh$'' - ''^tests/functional/local-overlay-store/bad-uris\.sh$'' - ''^tests/functional/local-overlay-store/build-inner\.sh$'' - ''^tests/functional/local-overlay-store/build\.sh$'' - ''^tests/functional/local-overlay-store/check-post-init-inner\.sh$'' - ''^tests/functional/local-overlay-store/check-post-init\.sh$'' - ''^tests/functional/local-overlay-store/common\.sh$'' - ''^tests/functional/local-overlay-store/delete-duplicate-inner\.sh$'' - ''^tests/functional/local-overlay-store/delete-duplicate\.sh$'' - ''^tests/functional/local-overlay-store/delete-refs-inner\.sh$'' - ''^tests/functional/local-overlay-store/delete-refs\.sh$'' - ''^tests/functional/local-overlay-store/gc-inner\.sh$'' - ''^tests/functional/local-overlay-store/gc\.sh$'' - ''^tests/functional/local-overlay-store/optimise-inner\.sh$'' - ''^tests/functional/local-overlay-store/optimise\.sh$'' - ''^tests/functional/local-overlay-store/redundant-add-inner\.sh$'' - ''^tests/functional/local-overlay-store/redundant-add\.sh$'' - ''^tests/functional/local-overlay-store/remount\.sh$'' - ''^tests/functional/local-overlay-store/stale-file-handle-inner\.sh$'' - ''^tests/functional/local-overlay-store/stale-file-handle\.sh$'' - ''^tests/functional/local-overlay-store/verify-inner\.sh$'' - ''^tests/functional/local-overlay-store/verify\.sh$'' - ''^tests/functional/logging\.sh$'' - ''^tests/functional/misc\.sh$'' - ''^tests/functional/multiple-outputs\.sh$'' - ''^tests/functional/nested-sandboxing\.sh$'' - ''^tests/functional/nested-sandboxing/command\.sh$'' - ''^tests/functional/nix-build\.sh$'' - ''^tests/functional/nix-channel\.sh$'' - ''^tests/functional/nix-collect-garbage-d\.sh$'' - ''^tests/functional/nix-copy-ssh-common\.sh$'' - ''^tests/functional/nix-copy-ssh-ng\.sh$'' - ''^tests/functional/nix-copy-ssh\.sh$'' - ''^tests/functional/nix-daemon-untrusting\.sh$'' - ''^tests/functional/nix-profile\.sh$'' - ''^tests/functional/nix-shell\.sh$'' - ''^tests/functional/nix_path\.sh$'' - ''^tests/functional/optimise-store\.sh$'' - ''^tests/functional/output-normalization\.sh$'' - ''^tests/functional/parallel\.builder\.sh$'' - ''^tests/functional/parallel\.sh$'' - ''^tests/functional/pass-as-file\.sh$'' - ''^tests/functional/path-from-hash-part\.sh$'' - ''^tests/functional/path-info\.sh$'' - ''^tests/functional/placeholders\.sh$'' - ''^tests/functional/post-hook\.sh$'' - ''^tests/functional/pure-eval\.sh$'' - ''^tests/functional/push-to-store-old\.sh$'' - ''^tests/functional/push-to-store\.sh$'' - ''^tests/functional/read-only-store\.sh$'' - ''^tests/functional/readfile-context\.sh$'' - ''^tests/functional/recursive\.sh$'' - ''^tests/functional/referrers\.sh$'' - ''^tests/functional/remote-store\.sh$'' - ''^tests/functional/repair\.sh$'' - ''^tests/functional/restricted\.sh$'' - ''^tests/functional/search\.sh$'' - ''^tests/functional/secure-drv-outputs\.sh$'' - ''^tests/functional/selfref-gc\.sh$'' - ''^tests/functional/shell\.shebang\.sh$'' - ''^tests/functional/simple\.builder\.sh$'' - ''^tests/functional/supplementary-groups\.sh$'' - ''^tests/functional/toString-path\.sh$'' - ''^tests/functional/user-envs-migration\.sh$'' - ''^tests/functional/user-envs-test-case\.sh$'' - ''^tests/functional/user-envs\.builder\.sh$'' - ''^tests/functional/user-envs\.sh$'' - ''^tests/functional/why-depends\.sh$'' - ''^src/libutil-tests/data/git/check-data\.sh$'' - ]; + # Not supported by nixfmt + ''^tests/functional/lang/eval-okay-deprecate-cursed-or\.nix$'' + ''^tests/functional/lang/eval-okay-attrs5\.nix$'' + + # More syntax tests + # These tests, or parts of them, should have been parse-* test cases. + ''^tests/functional/lang/eval-fail-eol-2\.nix$'' + ''^tests/functional/lang/eval-fail-path-slash\.nix$'' + ''^tests/functional/lang/eval-fail-toJSON-non-utf-8\.nix$'' + ''^tests/functional/lang/eval-fail-set\.nix$'' + ]; + }; + clang-format = { + enable = true; + # https://github.com/cachix/git-hooks.nix/pull/532 + package = pkgs.llvmPackages_latest.clang-tools; + excludes = [ + # We don't want to format test data + # ''tests/(?!nixos/).*\.nix'' + ''^src/[^/]*-tests/data/.*$'' + + # Don't format vendored code + ''^doc/manual/redirects\.js$'' + ''^doc/manual/theme/highlight\.js$'' + + # We haven't applied formatting to these files yet + ''^doc/manual/redirects\.js$'' + ''^doc/manual/theme/highlight\.js$'' + ''^src/build-remote/build-remote\.cc$'' + ''^src/libcmd/built-path\.cc$'' + ''^src/libcmd/include/nix/cmd/built-path\.hh$'' + ''^src/libcmd/common-eval-args\.cc$'' + ''^src/libcmd/include/nix/cmd/common-eval-args\.hh$'' + ''^src/libcmd/editor-for\.cc$'' + ''^src/libcmd/installable-attr-path\.cc$'' + ''^src/libcmd/include/nix/cmd/installable-attr-path\.hh$'' + ''^src/libcmd/installable-derived-path\.cc$'' + ''^src/libcmd/include/nix/cmd/installable-derived-path\.hh$'' + ''^src/libcmd/installable-flake\.cc$'' + ''^src/libcmd/include/nix/cmd/installable-flake\.hh$'' + ''^src/libcmd/installable-value\.cc$'' + ''^src/libcmd/include/nix/cmd/installable-value\.hh$'' + ''^src/libcmd/installables\.cc$'' + ''^src/libcmd/include/nix/cmd/installables\.hh$'' + ''^src/libcmd/include/nix/cmd/legacy\.hh$'' + ''^src/libcmd/markdown\.cc$'' + ''^src/libcmd/misc-store-flags\.cc$'' + ''^src/libcmd/repl-interacter\.cc$'' + ''^src/libcmd/include/nix/cmd/repl-interacter\.hh$'' + ''^src/libcmd/repl\.cc$'' + ''^src/libcmd/include/nix/cmd/repl\.hh$'' + ''^src/libexpr-c/nix_api_expr\.cc$'' + ''^src/libexpr-c/nix_api_external\.cc$'' + ''^src/libexpr/attr-path\.cc$'' + ''^src/libexpr/include/nix/expr/attr-path\.hh$'' + ''^src/libexpr/attr-set\.cc$'' + ''^src/libexpr/include/nix/expr/attr-set\.hh$'' + ''^src/libexpr/eval-cache\.cc$'' + ''^src/libexpr/include/nix/expr/eval-cache\.hh$'' + ''^src/libexpr/eval-error\.cc$'' + ''^src/libexpr/include/nix/expr/eval-inline\.hh$'' + ''^src/libexpr/eval-settings\.cc$'' + ''^src/libexpr/include/nix/expr/eval-settings\.hh$'' + ''^src/libexpr/eval\.cc$'' + ''^src/libexpr/include/nix/expr/eval\.hh$'' + ''^src/libexpr/function-trace\.cc$'' + ''^src/libexpr/include/nix/expr/gc-small-vector\.hh$'' + ''^src/libexpr/get-drvs\.cc$'' + ''^src/libexpr/include/nix/expr/get-drvs\.hh$'' + ''^src/libexpr/json-to-value\.cc$'' + ''^src/libexpr/nixexpr\.cc$'' + ''^src/libexpr/include/nix/expr/nixexpr\.hh$'' + ''^src/libexpr/include/nix/expr/parser-state\.hh$'' + ''^src/libexpr/primops\.cc$'' + ''^src/libexpr/include/nix/expr/primops\.hh$'' + ''^src/libexpr/primops/context\.cc$'' + ''^src/libexpr/primops/fetchClosure\.cc$'' + ''^src/libexpr/primops/fetchMercurial\.cc$'' + ''^src/libexpr/primops/fetchTree\.cc$'' + ''^src/libexpr/primops/fromTOML\.cc$'' + ''^src/libexpr/print-ambiguous\.cc$'' + ''^src/libexpr/include/nix/expr/print-ambiguous\.hh$'' + ''^src/libexpr/include/nix/expr/print-options\.hh$'' + ''^src/libexpr/print\.cc$'' + ''^src/libexpr/include/nix/expr/print\.hh$'' + ''^src/libexpr/search-path\.cc$'' + ''^src/libexpr/include/nix/expr/symbol-table\.hh$'' + ''^src/libexpr/value-to-json\.cc$'' + ''^src/libexpr/include/nix/expr/value-to-json\.hh$'' + ''^src/libexpr/value-to-xml\.cc$'' + ''^src/libexpr/include/nix/expr/value-to-xml\.hh$'' + ''^src/libexpr/value/context\.cc$'' + ''^src/libexpr/include/nix/expr/value/context\.hh$'' + ''^src/libfetchers/attrs\.cc$'' + ''^src/libfetchers/cache\.cc$'' + ''^src/libfetchers/include/nix/fetchers/cache\.hh$'' + ''^src/libfetchers/fetch-settings\.cc$'' + ''^src/libfetchers/include/nix/fetchers/fetch-settings\.hh$'' + ''^src/libfetchers/fetch-to-store\.cc$'' + ''^src/libfetchers/fetchers\.cc$'' + ''^src/libfetchers/include/nix/fetchers/fetchers\.hh$'' + ''^src/libfetchers/filtering-source-accessor\.cc$'' + ''^src/libfetchers/include/nix/fetchers/filtering-source-accessor\.hh$'' + ''^src/libfetchers/fs-source-accessor\.cc$'' + ''^src/libfetchers/include/nix/fs-source-accessor\.hh$'' + ''^src/libfetchers/git-utils\.cc$'' + ''^src/libfetchers/include/nix/fetchers/git-utils\.hh$'' + ''^src/libfetchers/github\.cc$'' + ''^src/libfetchers/indirect\.cc$'' + ''^src/libfetchers/memory-source-accessor\.cc$'' + ''^src/libfetchers/path\.cc$'' + ''^src/libfetchers/registry\.cc$'' + ''^src/libfetchers/include/nix/fetchers/registry\.hh$'' + ''^src/libfetchers/tarball\.cc$'' + ''^src/libfetchers/include/nix/fetchers/tarball\.hh$'' + ''^src/libfetchers/git\.cc$'' + ''^src/libfetchers/mercurial\.cc$'' + ''^src/libflake/config\.cc$'' + ''^src/libflake/flake\.cc$'' + ''^src/libflake/include/nix/flake/flake\.hh$'' + ''^src/libflake/flakeref\.cc$'' + ''^src/libflake/include/nix/flake/flakeref\.hh$'' + ''^src/libflake/lockfile\.cc$'' + ''^src/libflake/include/nix/flake/lockfile\.hh$'' + ''^src/libflake/url-name\.cc$'' + ''^src/libmain/common-args\.cc$'' + ''^src/libmain/include/nix/main/common-args\.hh$'' + ''^src/libmain/loggers\.cc$'' + ''^src/libmain/include/nix/main/loggers\.hh$'' + ''^src/libmain/progress-bar\.cc$'' + ''^src/libmain/shared\.cc$'' + ''^src/libmain/include/nix/main/shared\.hh$'' + ''^src/libmain/unix/stack\.cc$'' + ''^src/libstore/binary-cache-store\.cc$'' + ''^src/libstore/include/nix/store/binary-cache-store\.hh$'' + ''^src/libstore/include/nix/store/build-result\.hh$'' + ''^src/libstore/include/nix/store/builtins\.hh$'' + ''^src/libstore/builtins/buildenv\.cc$'' + ''^src/libstore/include/nix/store/builtins/buildenv\.hh$'' + ''^src/libstore/include/nix/store/common-protocol-impl\.hh$'' + ''^src/libstore/common-protocol\.cc$'' + ''^src/libstore/include/nix/store/common-protocol\.hh$'' + ''^src/libstore/include/nix/store/common-ssh-store-config\.hh$'' + ''^src/libstore/content-address\.cc$'' + ''^src/libstore/include/nix/store/content-address\.hh$'' + ''^src/libstore/daemon\.cc$'' + ''^src/libstore/include/nix/store/daemon\.hh$'' + ''^src/libstore/derivations\.cc$'' + ''^src/libstore/include/nix/store/derivations\.hh$'' + ''^src/libstore/derived-path-map\.cc$'' + ''^src/libstore/include/nix/store/derived-path-map\.hh$'' + ''^src/libstore/derived-path\.cc$'' + ''^src/libstore/include/nix/store/derived-path\.hh$'' + ''^src/libstore/downstream-placeholder\.cc$'' + ''^src/libstore/include/nix/store/downstream-placeholder\.hh$'' + ''^src/libstore/dummy-store\.cc$'' + ''^src/libstore/export-import\.cc$'' + ''^src/libstore/filetransfer\.cc$'' + ''^src/libstore/include/nix/store/filetransfer\.hh$'' + ''^src/libstore/include/nix/store/gc-store\.hh$'' + ''^src/libstore/globals\.cc$'' + ''^src/libstore/include/nix/store/globals\.hh$'' + ''^src/libstore/http-binary-cache-store\.cc$'' + ''^src/libstore/legacy-ssh-store\.cc$'' + ''^src/libstore/include/nix/store/legacy-ssh-store\.hh$'' + ''^src/libstore/include/nix/store/length-prefixed-protocol-helper\.hh$'' + ''^src/libstore/linux/personality\.cc$'' + ''^src/libstore/linux/include/nix/store/personality\.hh$'' + ''^src/libstore/local-binary-cache-store\.cc$'' + ''^src/libstore/local-fs-store\.cc$'' + ''^src/libstore/include/nix/store/local-fs-store\.hh$'' + ''^src/libstore/log-store\.cc$'' + ''^src/libstore/include/nix/store/log-store\.hh$'' + ''^src/libstore/machines\.cc$'' + ''^src/libstore/include/nix/store/machines\.hh$'' + ''^src/libstore/make-content-addressed\.cc$'' + ''^src/libstore/include/nix/store/make-content-addressed\.hh$'' + ''^src/libstore/misc\.cc$'' + ''^src/libstore/names\.cc$'' + ''^src/libstore/include/nix/store/names\.hh$'' + ''^src/libstore/nar-accessor\.cc$'' + ''^src/libstore/include/nix/store/nar-accessor\.hh$'' + ''^src/libstore/nar-info-disk-cache\.cc$'' + ''^src/libstore/include/nix/store/nar-info-disk-cache\.hh$'' + ''^src/libstore/nar-info\.cc$'' + ''^src/libstore/include/nix/store/nar-info\.hh$'' + ''^src/libstore/outputs-spec\.cc$'' + ''^src/libstore/include/nix/store/outputs-spec\.hh$'' + ''^src/libstore/parsed-derivations\.cc$'' + ''^src/libstore/path-info\.cc$'' + ''^src/libstore/include/nix/store/path-info\.hh$'' + ''^src/libstore/path-references\.cc$'' + ''^src/libstore/include/nix/store/path-regex\.hh$'' + ''^src/libstore/path-with-outputs\.cc$'' + ''^src/libstore/path\.cc$'' + ''^src/libstore/include/nix/store/path\.hh$'' + ''^src/libstore/pathlocks\.cc$'' + ''^src/libstore/include/nix/store/pathlocks\.hh$'' + ''^src/libstore/profiles\.cc$'' + ''^src/libstore/include/nix/store/profiles\.hh$'' + ''^src/libstore/realisation\.cc$'' + ''^src/libstore/include/nix/store/realisation\.hh$'' + ''^src/libstore/remote-fs-accessor\.cc$'' + ''^src/libstore/include/nix/store/remote-fs-accessor\.hh$'' + ''^src/libstore/include/nix/store/remote-store-connection\.hh$'' + ''^src/libstore/remote-store\.cc$'' + ''^src/libstore/include/nix/store/remote-store\.hh$'' + ''^src/libstore/s3-binary-cache-store\.cc$'' + ''^src/libstore/include/nix/store/s3\.hh$'' + ''^src/libstore/serve-protocol-impl\.cc$'' + ''^src/libstore/include/nix/store/serve-protocol-impl\.hh$'' + ''^src/libstore/serve-protocol\.cc$'' + ''^src/libstore/include/nix/store/serve-protocol\.hh$'' + ''^src/libstore/sqlite\.cc$'' + ''^src/libstore/include/nix/store/sqlite\.hh$'' + ''^src/libstore/ssh-store\.cc$'' + ''^src/libstore/ssh\.cc$'' + ''^src/libstore/include/nix/store/ssh\.hh$'' + ''^src/libstore/store-api\.cc$'' + ''^src/libstore/include/nix/store/store-api\.hh$'' + ''^src/libstore/include/nix/store/store-dir-config\.hh$'' + ''^src/libstore/build/derivation-building-goal\.cc$'' + ''^src/libstore/include/nix/store/build/derivation-building-goal\.hh$'' + ''^src/libstore/build/derivation-goal\.cc$'' + ''^src/libstore/include/nix/store/build/derivation-goal\.hh$'' + ''^src/libstore/build/drv-output-substitution-goal\.cc$'' + ''^src/libstore/include/nix/store/build/drv-output-substitution-goal\.hh$'' + ''^src/libstore/build/entry-points\.cc$'' + ''^src/libstore/build/goal\.cc$'' + ''^src/libstore/include/nix/store/build/goal\.hh$'' + ''^src/libstore/unix/build/hook-instance\.cc$'' + ''^src/libstore/unix/build/derivation-builder\.cc$'' + ''^src/libstore/unix/include/nix/store/build/derivation-builder\.hh$'' + ''^src/libstore/build/substitution-goal\.cc$'' + ''^src/libstore/include/nix/store/build/substitution-goal\.hh$'' + ''^src/libstore/build/worker\.cc$'' + ''^src/libstore/include/nix/store/build/worker\.hh$'' + ''^src/libstore/builtins/fetchurl\.cc$'' + ''^src/libstore/builtins/unpack-channel\.cc$'' + ''^src/libstore/gc\.cc$'' + ''^src/libstore/local-overlay-store\.cc$'' + ''^src/libstore/include/nix/store/local-overlay-store\.hh$'' + ''^src/libstore/local-store\.cc$'' + ''^src/libstore/include/nix/store/local-store\.hh$'' + ''^src/libstore/unix/user-lock\.cc$'' + ''^src/libstore/unix/include/nix/store/user-lock\.hh$'' + ''^src/libstore/optimise-store\.cc$'' + ''^src/libstore/unix/pathlocks\.cc$'' + ''^src/libstore/posix-fs-canonicalise\.cc$'' + ''^src/libstore/include/nix/store/posix-fs-canonicalise\.hh$'' + ''^src/libstore/uds-remote-store\.cc$'' + ''^src/libstore/include/nix/store/uds-remote-store\.hh$'' + ''^src/libstore/windows/build\.cc$'' + ''^src/libstore/include/nix/store/worker-protocol-impl\.hh$'' + ''^src/libstore/worker-protocol\.cc$'' + ''^src/libstore/include/nix/store/worker-protocol\.hh$'' + ''^src/libutil-c/nix_api_util_internal\.h$'' + ''^src/libutil/archive\.cc$'' + ''^src/libutil/include/nix/util/archive\.hh$'' + ''^src/libutil/args\.cc$'' + ''^src/libutil/include/nix/util/args\.hh$'' + ''^src/libutil/include/nix/util/args/root\.hh$'' + ''^src/libutil/include/nix/util/callback\.hh$'' + ''^src/libutil/canon-path\.cc$'' + ''^src/libutil/include/nix/util/canon-path\.hh$'' + ''^src/libutil/include/nix/util/chunked-vector\.hh$'' + ''^src/libutil/include/nix/util/closure\.hh$'' + ''^src/libutil/include/nix/util/comparator\.hh$'' + ''^src/libutil/compute-levels\.cc$'' + ''^src/libutil/include/nix/util/config-impl\.hh$'' + ''^src/libutil/configuration\.cc$'' + ''^src/libutil/include/nix/util/configuration\.hh$'' + ''^src/libutil/current-process\.cc$'' + ''^src/libutil/include/nix/util/current-process\.hh$'' + ''^src/libutil/english\.cc$'' + ''^src/libutil/include/nix/util/english\.hh$'' + ''^src/libutil/error\.cc$'' + ''^src/libutil/include/nix/util/error\.hh$'' + ''^src/libutil/include/nix/util/exit\.hh$'' + ''^src/libutil/experimental-features\.cc$'' + ''^src/libutil/include/nix/util/experimental-features\.hh$'' + ''^src/libutil/file-content-address\.cc$'' + ''^src/libutil/include/nix/util/file-content-address\.hh$'' + ''^src/libutil/file-descriptor\.cc$'' + ''^src/libutil/include/nix/util/file-descriptor\.hh$'' + ''^src/libutil/include/nix/util/file-path-impl\.hh$'' + ''^src/libutil/include/nix/util/file-path\.hh$'' + ''^src/libutil/file-system\.cc$'' + ''^src/libutil/include/nix/util/file-system\.hh$'' + ''^src/libutil/include/nix/util/finally\.hh$'' + ''^src/libutil/include/nix/util/fmt\.hh$'' + ''^src/libutil/fs-sink\.cc$'' + ''^src/libutil/include/nix/util/fs-sink\.hh$'' + ''^src/libutil/git\.cc$'' + ''^src/libutil/include/nix/util/git\.hh$'' + ''^src/libutil/hash\.cc$'' + ''^src/libutil/include/nix/util/hash\.hh$'' + ''^src/libutil/hilite\.cc$'' + ''^src/libutil/include/nix/util/hilite\.hh$'' + ''^src/libutil/source-accessor\.hh$'' + ''^src/libutil/include/nix/util/json-impls\.hh$'' + ''^src/libutil/json-utils\.cc$'' + ''^src/libutil/include/nix/util/json-utils\.hh$'' + ''^src/libutil/linux/cgroup\.cc$'' + ''^src/libutil/linux/linux-namespaces\.cc$'' + ''^src/libutil/logging\.cc$'' + ''^src/libutil/include/nix/util/logging\.hh$'' + ''^src/libutil/memory-source-accessor\.cc$'' + ''^src/libutil/include/nix/util/memory-source-accessor\.hh$'' + ''^src/libutil/include/nix/util/pool\.hh$'' + ''^src/libutil/position\.cc$'' + ''^src/libutil/include/nix/util/position\.hh$'' + ''^src/libutil/posix-source-accessor\.cc$'' + ''^src/libutil/include/nix/util/posix-source-accessor\.hh$'' + ''^src/libutil/include/nix/util/processes\.hh$'' + ''^src/libutil/include/nix/util/ref\.hh$'' + ''^src/libutil/references\.cc$'' + ''^src/libutil/include/nix/util/references\.hh$'' + ''^src/libutil/regex-combinators\.hh$'' + ''^src/libutil/serialise\.cc$'' + ''^src/libutil/include/nix/util/serialise\.hh$'' + ''^src/libutil/include/nix/util/signals\.hh$'' + ''^src/libutil/signature/local-keys\.cc$'' + ''^src/libutil/include/nix/util/signature/local-keys\.hh$'' + ''^src/libutil/signature/signer\.cc$'' + ''^src/libutil/include/nix/util/signature/signer\.hh$'' + ''^src/libutil/source-accessor\.cc$'' + ''^src/libutil/include/nix/util/source-accessor\.hh$'' + ''^src/libutil/source-path\.cc$'' + ''^src/libutil/include/nix/util/source-path\.hh$'' + ''^src/libutil/include/nix/util/split\.hh$'' + ''^src/libutil/suggestions\.cc$'' + ''^src/libutil/include/nix/util/suggestions\.hh$'' + ''^src/libutil/include/nix/util/sync\.hh$'' + ''^src/libutil/terminal\.cc$'' + ''^src/libutil/include/nix/util/terminal\.hh$'' + ''^src/libutil/thread-pool\.cc$'' + ''^src/libutil/include/nix/util/thread-pool\.hh$'' + ''^src/libutil/include/nix/util/topo-sort\.hh$'' + ''^src/libutil/include/nix/util/types\.hh$'' + ''^src/libutil/unix/file-descriptor\.cc$'' + ''^src/libutil/unix/file-path\.cc$'' + ''^src/libutil/unix/processes\.cc$'' + ''^src/libutil/unix/include/nix/util/signals-impl\.hh$'' + ''^src/libutil/unix/signals\.cc$'' + ''^src/libutil/unix-domain-socket\.cc$'' + ''^src/libutil/unix/users\.cc$'' + ''^src/libutil/include/nix/util/url-parts\.hh$'' + ''^src/libutil/url\.cc$'' + ''^src/libutil/include/nix/util/url\.hh$'' + ''^src/libutil/users\.cc$'' + ''^src/libutil/include/nix/util/users\.hh$'' + ''^src/libutil/util\.cc$'' + ''^src/libutil/include/nix/util/util\.hh$'' + ''^src/libutil/include/nix/util/variant-wrapper\.hh$'' + ''^src/libutil/widecharwidth/widechar_width\.h$'' # vendored source + ''^src/libutil/windows/file-descriptor\.cc$'' + ''^src/libutil/windows/file-path\.cc$'' + ''^src/libutil/windows/processes\.cc$'' + ''^src/libutil/windows/users\.cc$'' + ''^src/libutil/windows/windows-error\.cc$'' + ''^src/libutil/windows/include/nix/util/windows-error\.hh$'' + ''^src/libutil/xml-writer\.cc$'' + ''^src/libutil/include/nix/util/xml-writer\.hh$'' + ''^src/nix-build/nix-build\.cc$'' + ''^src/nix-channel/nix-channel\.cc$'' + ''^src/nix-collect-garbage/nix-collect-garbage\.cc$'' + ''^src/nix-env/buildenv.nix$'' + ''^src/nix-env/nix-env\.cc$'' + ''^src/nix-env/user-env\.cc$'' + ''^src/nix-env/user-env\.hh$'' + ''^src/nix-instantiate/nix-instantiate\.cc$'' + ''^src/nix-store/dotgraph\.cc$'' + ''^src/nix-store/graphml\.cc$'' + ''^src/nix-store/nix-store\.cc$'' + ''^src/nix/add-to-store\.cc$'' + ''^src/nix/app\.cc$'' + ''^src/nix/build\.cc$'' + ''^src/nix/bundle\.cc$'' + ''^src/nix/cat\.cc$'' + ''^src/nix/config-check\.cc$'' + ''^src/nix/config\.cc$'' + ''^src/nix/copy\.cc$'' + ''^src/nix/derivation-add\.cc$'' + ''^src/nix/derivation-show\.cc$'' + ''^src/nix/derivation\.cc$'' + ''^src/nix/develop\.cc$'' + ''^src/nix/diff-closures\.cc$'' + ''^src/nix/dump-path\.cc$'' + ''^src/nix/edit\.cc$'' + ''^src/nix/eval\.cc$'' + ''^src/nix/flake\.cc$'' + ''^src/nix/fmt\.cc$'' + ''^src/nix/hash\.cc$'' + ''^src/nix/log\.cc$'' + ''^src/nix/ls\.cc$'' + ''^src/nix/main\.cc$'' + ''^src/nix/make-content-addressed\.cc$'' + ''^src/nix/nar\.cc$'' + ''^src/nix/optimise-store\.cc$'' + ''^src/nix/path-from-hash-part\.cc$'' + ''^src/nix/path-info\.cc$'' + ''^src/nix/prefetch\.cc$'' + ''^src/nix/profile\.cc$'' + ''^src/nix/realisation\.cc$'' + ''^src/nix/registry\.cc$'' + ''^src/nix/repl\.cc$'' + ''^src/nix/run\.cc$'' + ''^src/nix/run\.hh$'' + ''^src/nix/search\.cc$'' + ''^src/nix/sigs\.cc$'' + ''^src/nix/store-copy-log\.cc$'' + ''^src/nix/store-delete\.cc$'' + ''^src/nix/store-gc\.cc$'' + ''^src/nix/store-info\.cc$'' + ''^src/nix/store-repair\.cc$'' + ''^src/nix/store\.cc$'' + ''^src/nix/unix/daemon\.cc$'' + ''^src/nix/upgrade-nix\.cc$'' + ''^src/nix/verify\.cc$'' + ''^src/nix/why-depends\.cc$'' + + ''^tests/functional/plugins/plugintest\.cc'' + ''^tests/functional/test-libstoreconsumer/main\.cc'' + ''^tests/nixos/ca-fd-leak/sender\.c'' + ''^tests/nixos/ca-fd-leak/smuggler\.c'' + ''^tests/nixos/user-sandboxing/attacker\.c'' + ''^src/libexpr-test-support/include/nix/expr/tests/libexpr\.hh'' + ''^src/libexpr-test-support/tests/value/context\.cc'' + ''^src/libexpr-test-support/include/nix/expr/tests/value/context\.hh'' + ''^src/libexpr-tests/derived-path\.cc'' + ''^src/libexpr-tests/error_traces\.cc'' + ''^src/libexpr-tests/eval\.cc'' + ''^src/libexpr-tests/json\.cc'' + ''^src/libexpr-tests/main\.cc'' + ''^src/libexpr-tests/primops\.cc'' + ''^src/libexpr-tests/search-path\.cc'' + ''^src/libexpr-tests/trivial\.cc'' + ''^src/libexpr-tests/value/context\.cc'' + ''^src/libexpr-tests/value/print\.cc'' + ''^src/libfetchers-tests/public-key\.cc'' + ''^src/libflake-tests/flakeref\.cc'' + ''^src/libflake-tests/url-name\.cc'' + ''^src/libstore-test-support/tests/derived-path\.cc'' + ''^src/libstore-test-support/include/nix/store/tests/derived-path\.hh'' + ''^src/libstore-test-support/include/nix/store/tests/nix_api_store\.hh'' + ''^src/libstore-test-support/tests/outputs-spec\.cc'' + ''^src/libstore-test-support/include/nix/store/tests/outputs-spec\.hh'' + ''^src/libstore-test-support/path\.cc'' + ''^src/libstore-test-support/include/nix/store/tests/path\.hh'' + ''^src/libstore-test-support/include/nix/store/tests/protocol\.hh'' + ''^src/libstore-tests/common-protocol\.cc'' + ''^src/libstore-tests/content-address\.cc'' + ''^src/libstore-tests/derivation\.cc'' + ''^src/libstore-tests/derived-path\.cc'' + ''^src/libstore-tests/downstream-placeholder\.cc'' + ''^src/libstore-tests/machines\.cc'' + ''^src/libstore-tests/nar-info-disk-cache\.cc'' + ''^src/libstore-tests/nar-info\.cc'' + ''^src/libstore-tests/outputs-spec\.cc'' + ''^src/libstore-tests/path-info\.cc'' + ''^src/libstore-tests/path\.cc'' + ''^src/libstore-tests/serve-protocol\.cc'' + ''^src/libstore-tests/worker-protocol\.cc'' + ''^src/libutil-test-support/include/nix/util/tests/characterization\.hh'' + ''^src/libutil-test-support/hash\.cc'' + ''^src/libutil-test-support/include/nix/util/tests/hash\.hh'' + ''^src/libutil-tests/args\.cc'' + ''^src/libutil-tests/canon-path\.cc'' + ''^src/libutil-tests/chunked-vector\.cc'' + ''^src/libutil-tests/closure\.cc'' + ''^src/libutil-tests/compression\.cc'' + ''^src/libutil-tests/config\.cc'' + ''^src/libutil-tests/file-content-address\.cc'' + ''^src/libutil-tests/git\.cc'' + ''^src/libutil-tests/hash\.cc'' + ''^src/libutil-tests/hilite\.cc'' + ''^src/libutil-tests/json-utils\.cc'' + ''^src/libutil-tests/logging\.cc'' + ''^src/libutil-tests/lru-cache\.cc'' + ''^src/libutil-tests/pool\.cc'' + ''^src/libutil-tests/references\.cc'' + ''^src/libutil-tests/suggestions\.cc'' + ''^src/libutil-tests/url\.cc'' + ''^src/libutil-tests/xml-writer\.cc'' + ]; + }; + shellcheck = { + enable = true; + excludes = [ + # We haven't linted these files yet + ''^config/install-sh$'' + ''^misc/bash/completion\.sh$'' + ''^misc/fish/completion\.fish$'' + ''^misc/zsh/completion\.zsh$'' + ''^scripts/create-darwin-volume\.sh$'' + ''^scripts/install-darwin-multi-user\.sh$'' + ''^scripts/install-multi-user\.sh$'' + ''^scripts/install-systemd-multi-user\.sh$'' + ''^src/nix/get-env\.sh$'' + ''^tests/functional/ca/build-dry\.sh$'' + ''^tests/functional/ca/build-with-garbage-path\.sh$'' + ''^tests/functional/ca/common\.sh$'' + ''^tests/functional/ca/concurrent-builds\.sh$'' + ''^tests/functional/ca/eval-store\.sh$'' + ''^tests/functional/ca/gc\.sh$'' + ''^tests/functional/ca/import-from-derivation\.sh$'' + ''^tests/functional/ca/new-build-cmd\.sh$'' + ''^tests/functional/ca/nix-shell\.sh$'' + ''^tests/functional/ca/post-hook\.sh$'' + ''^tests/functional/ca/recursive\.sh$'' + ''^tests/functional/ca/repl\.sh$'' + ''^tests/functional/ca/selfref-gc\.sh$'' + ''^tests/functional/ca/why-depends\.sh$'' + ''^tests/functional/characterisation-test-infra\.sh$'' + ''^tests/functional/common/vars-and-functions\.sh$'' + ''^tests/functional/completions\.sh$'' + ''^tests/functional/compute-levels\.sh$'' + ''^tests/functional/config\.sh$'' + ''^tests/functional/db-migration\.sh$'' + ''^tests/functional/debugger\.sh$'' + ''^tests/functional/dependencies\.builder0\.sh$'' + ''^tests/functional/dependencies\.sh$'' + ''^tests/functional/dump-db\.sh$'' + ''^tests/functional/dyn-drv/build-built-drv\.sh$'' + ''^tests/functional/dyn-drv/common\.sh$'' + ''^tests/functional/dyn-drv/dep-built-drv\.sh$'' + ''^tests/functional/dyn-drv/eval-outputOf\.sh$'' + ''^tests/functional/dyn-drv/old-daemon-error-hack\.sh$'' + ''^tests/functional/dyn-drv/recursive-mod-json\.sh$'' + ''^tests/functional/eval-store\.sh$'' + ''^tests/functional/export-graph\.sh$'' + ''^tests/functional/export\.sh$'' + ''^tests/functional/extra-sandbox-profile\.sh$'' + ''^tests/functional/fetchClosure\.sh$'' + ''^tests/functional/fetchGit\.sh$'' + ''^tests/functional/fetchGitRefs\.sh$'' + ''^tests/functional/fetchGitSubmodules\.sh$'' + ''^tests/functional/fetchGitVerification\.sh$'' + ''^tests/functional/fetchMercurial\.sh$'' + ''^tests/functional/fixed\.builder1\.sh$'' + ''^tests/functional/fixed\.builder2\.sh$'' + ''^tests/functional/fixed\.sh$'' + ''^tests/functional/flakes/absolute-paths\.sh$'' + ''^tests/functional/flakes/check\.sh$'' + ''^tests/functional/flakes/config\.sh$'' + ''^tests/functional/flakes/flakes\.sh$'' + ''^tests/functional/flakes/follow-paths\.sh$'' + ''^tests/functional/flakes/prefetch\.sh$'' + ''^tests/functional/flakes/run\.sh$'' + ''^tests/functional/flakes/show\.sh$'' + ''^tests/functional/formatter\.sh$'' + ''^tests/functional/formatter\.simple\.sh$'' + ''^tests/functional/gc-auto\.sh$'' + ''^tests/functional/gc-concurrent\.builder\.sh$'' + ''^tests/functional/gc-concurrent\.sh$'' + ''^tests/functional/gc-concurrent2\.builder\.sh$'' + ''^tests/functional/gc-non-blocking\.sh$'' + ''^tests/functional/git-hashing/common\.sh$'' + ''^tests/functional/git-hashing/simple\.sh$'' + ''^tests/functional/hash-convert\.sh$'' + ''^tests/functional/impure-derivations\.sh$'' + ''^tests/functional/impure-eval\.sh$'' + ''^tests/functional/install-darwin\.sh$'' + ''^tests/functional/legacy-ssh-store\.sh$'' + ''^tests/functional/linux-sandbox\.sh$'' + ''^tests/functional/local-overlay-store/add-lower-inner\.sh$'' + ''^tests/functional/local-overlay-store/add-lower\.sh$'' + ''^tests/functional/local-overlay-store/bad-uris\.sh$'' + ''^tests/functional/local-overlay-store/build-inner\.sh$'' + ''^tests/functional/local-overlay-store/build\.sh$'' + ''^tests/functional/local-overlay-store/check-post-init-inner\.sh$'' + ''^tests/functional/local-overlay-store/check-post-init\.sh$'' + ''^tests/functional/local-overlay-store/common\.sh$'' + ''^tests/functional/local-overlay-store/delete-duplicate-inner\.sh$'' + ''^tests/functional/local-overlay-store/delete-duplicate\.sh$'' + ''^tests/functional/local-overlay-store/delete-refs-inner\.sh$'' + ''^tests/functional/local-overlay-store/delete-refs\.sh$'' + ''^tests/functional/local-overlay-store/gc-inner\.sh$'' + ''^tests/functional/local-overlay-store/gc\.sh$'' + ''^tests/functional/local-overlay-store/optimise-inner\.sh$'' + ''^tests/functional/local-overlay-store/optimise\.sh$'' + ''^tests/functional/local-overlay-store/redundant-add-inner\.sh$'' + ''^tests/functional/local-overlay-store/redundant-add\.sh$'' + ''^tests/functional/local-overlay-store/remount\.sh$'' + ''^tests/functional/local-overlay-store/stale-file-handle-inner\.sh$'' + ''^tests/functional/local-overlay-store/stale-file-handle\.sh$'' + ''^tests/functional/local-overlay-store/verify-inner\.sh$'' + ''^tests/functional/local-overlay-store/verify\.sh$'' + ''^tests/functional/logging\.sh$'' + ''^tests/functional/misc\.sh$'' + ''^tests/functional/multiple-outputs\.sh$'' + ''^tests/functional/nested-sandboxing\.sh$'' + ''^tests/functional/nested-sandboxing/command\.sh$'' + ''^tests/functional/nix-build\.sh$'' + ''^tests/functional/nix-channel\.sh$'' + ''^tests/functional/nix-collect-garbage-d\.sh$'' + ''^tests/functional/nix-copy-ssh-common\.sh$'' + ''^tests/functional/nix-copy-ssh-ng\.sh$'' + ''^tests/functional/nix-copy-ssh\.sh$'' + ''^tests/functional/nix-daemon-untrusting\.sh$'' + ''^tests/functional/nix-profile\.sh$'' + ''^tests/functional/nix-shell\.sh$'' + ''^tests/functional/nix_path\.sh$'' + ''^tests/functional/optimise-store\.sh$'' + ''^tests/functional/output-normalization\.sh$'' + ''^tests/functional/parallel\.builder\.sh$'' + ''^tests/functional/parallel\.sh$'' + ''^tests/functional/pass-as-file\.sh$'' + ''^tests/functional/path-from-hash-part\.sh$'' + ''^tests/functional/path-info\.sh$'' + ''^tests/functional/placeholders\.sh$'' + ''^tests/functional/post-hook\.sh$'' + ''^tests/functional/pure-eval\.sh$'' + ''^tests/functional/push-to-store-old\.sh$'' + ''^tests/functional/push-to-store\.sh$'' + ''^tests/functional/read-only-store\.sh$'' + ''^tests/functional/readfile-context\.sh$'' + ''^tests/functional/recursive\.sh$'' + ''^tests/functional/referrers\.sh$'' + ''^tests/functional/remote-store\.sh$'' + ''^tests/functional/repair\.sh$'' + ''^tests/functional/restricted\.sh$'' + ''^tests/functional/search\.sh$'' + ''^tests/functional/secure-drv-outputs\.sh$'' + ''^tests/functional/selfref-gc\.sh$'' + ''^tests/functional/shell\.shebang\.sh$'' + ''^tests/functional/simple\.builder\.sh$'' + ''^tests/functional/supplementary-groups\.sh$'' + ''^tests/functional/toString-path\.sh$'' + ''^tests/functional/user-envs-migration\.sh$'' + ''^tests/functional/user-envs-test-case\.sh$'' + ''^tests/functional/user-envs\.builder\.sh$'' + ''^tests/functional/user-envs\.sh$'' + ''^tests/functional/why-depends\.sh$'' + ''^src/libutil-tests/data/git/check-data\.sh$'' + ]; + }; }; - # TODO: nixfmt, https://github.com/NixOS/nixfmt/issues/153 }; }; - }; # We'll be pulling from this in the main flake flake.getSystem = getSystem; diff --git a/maintainers/release-credits b/maintainers/release-credits index 7a5c87d7dfb..10ffd48b586 100755 --- a/maintainers/release-credits +++ b/maintainers/release-credits @@ -109,15 +109,15 @@ for sample in samples: s = samples[sample] email = s["email"] if not email in email_to_handle_cache.values: - print(f"Querying GitHub API for {s['hash']}, to get handle for {s['email']}") + print(f"Querying GitHub API for {s['hash']}, to get handle for {s['email']}", file=sys.stderr) ghc = get_github_commit(samples[sample]) gha = ghc["author"] if gha and gha["login"]: handle = gha["login"] - print(f"Handle: {handle}") + print(f"Handle: {handle}", file=sys.stderr) email_to_handle_cache.values[email] = handle else: - print(f"Found no handle for {s['email']}") + print(f"Found no handle for {s['email']}", file=sys.stderr) email_to_handle_cache.values[email] = None handle = email_to_handle_cache.values[email] if handle is not None: diff --git a/maintainers/release-notes b/maintainers/release-notes index 0cdcd517bda..5bb492227b7 100755 --- a/maintainers/release-notes +++ b/maintainers/release-notes @@ -2,6 +2,8 @@ # vim: set filetype=bash: #!nix shell .#changelog-d --command bash +set -euo pipefail + # --- CONFIGURATION --- # This does double duty for @@ -155,7 +157,7 @@ section_title="Release $version_full ($DATE)" if ! $IS_PATCH; then echo - echo "# Contributors" + echo "## Contributors" echo VERSION=$version_full ./maintainers/release-credits fi diff --git a/maintainers/release-process.md b/maintainers/release-process.md index bf3c308cf9a..37b38fb9f7a 100644 --- a/maintainers/release-process.md +++ b/maintainers/release-process.md @@ -39,10 +39,6 @@ release: * Proof-read / edit / rearrange the release notes if needed. Breaking changes and highlights should go to the top. -* Run `maintainers/release-credits` to make sure the credits script works - and produces a sensible output. Some emails might not automatically map to - a GitHub handle. - * Push. ```console @@ -144,12 +140,10 @@ release: Make a pull request and auto-merge it. -* Create a milestone for the next release, move all unresolved issues - from the previous milestone, and close the previous milestone. Set - the date for the next milestone 6 weeks from now. - * Create a backport label. +* Add the new backport label to `.mergify.yml`. + * Post an [announcement on Discourse](https://discourse.nixos.org/c/announcements/8), including the contents of `rl-$VERSION.md`. diff --git a/maintainers/upload-release.pl b/maintainers/upload-release.pl index 8a470c7cc34..31a9c71d543 100755 --- a/maintainers/upload-release.pl +++ b/maintainers/upload-release.pl @@ -42,7 +42,7 @@ sub fetch { my $flakeInfo = decode_json(`nix flake metadata --json "$flakeUrl"` or die) if $flakeUrl; my $nixRev = ($flakeInfo ? $flakeInfo->{revision} : $evalInfo->{jobsetevalinputs}->{nix}->{revision}) or die; -my $buildInfo = decode_json(fetch("$evalUrl/job/build.nix.x86_64-linux", 'application/json')); +my $buildInfo = decode_json(fetch("$evalUrl/job/build.nix-everything.x86_64-linux", 'application/json')); #print Dumper($buildInfo); my $releaseName = $buildInfo->{nixname}; @@ -91,7 +91,7 @@ sub getStorePath { sub copyManual { my $manual; eval { - $manual = getStorePath("build.nix.x86_64-linux", "doc"); + $manual = getStorePath("manual"); }; if ($@) { warn "$@"; @@ -240,12 +240,12 @@ sub downloadFile { # Upload nix-fallback-paths.nix. write_file("$tmpDir/fallback-paths.nix", "{\n" . - " x86_64-linux = \"" . getStorePath("build.nix.x86_64-linux") . "\";\n" . - " i686-linux = \"" . getStorePath("build.nix.i686-linux") . "\";\n" . - " aarch64-linux = \"" . getStorePath("build.nix.aarch64-linux") . "\";\n" . - " riscv64-linux = \"" . getStorePath("buildCross.nix.riscv64-unknown-linux-gnu.x86_64-linux") . "\";\n" . - " x86_64-darwin = \"" . getStorePath("build.nix.x86_64-darwin") . "\";\n" . - " aarch64-darwin = \"" . getStorePath("build.nix.aarch64-darwin") . "\";\n" . + " x86_64-linux = \"" . getStorePath("build.nix-everything.x86_64-linux") . "\";\n" . + " i686-linux = \"" . getStorePath("build.nix-everything.i686-linux") . "\";\n" . + " aarch64-linux = \"" . getStorePath("build.nix-everything.aarch64-linux") . "\";\n" . + " riscv64-linux = \"" . getStorePath("buildCross.nix-everything.riscv64-unknown-linux-gnu.x86_64-linux") . "\";\n" . + " x86_64-darwin = \"" . getStorePath("build.nix-everything.x86_64-darwin") . "\";\n" . + " aarch64-darwin = \"" . getStorePath("build.nix-everything.aarch64-darwin") . "\";\n" . "}\n"); # Upload release files to S3. diff --git a/meson.build b/meson.build index 49adf9832ed..4a3a517fb2d 100644 --- a/meson.build +++ b/meson.build @@ -1,12 +1,16 @@ # This is just a stub project to include all the others as subprojects # for development shell purposes -project('nix-dev-shell', 'cpp', +project( + 'nix-dev-shell', + 'cpp', version : files('.version'), subproject_dir : 'src', default_options : [ 'localstatedir=/nix/var', - ] + # hack for trailing newline + ], + meson_version : '>= 1.1', ) # Internal Libraries @@ -33,6 +37,7 @@ endif # External C wrapper libraries subproject('libutil-c') subproject('libstore-c') +subproject('libfetchers-c') subproject('libexpr-c') subproject('libflake-c') subproject('libmain-c') diff --git a/meson.format b/meson.format new file mode 100644 index 00000000000..4876dd962db --- /dev/null +++ b/meson.format @@ -0,0 +1,7 @@ +indent_by = ' ' +space_array = true +kwargs_force_multiline = false +wide_colon = true +group_arg_value = true +indent_before_comments = ' ' +use_editor_config = true diff --git a/meson.options b/meson.options index 329fe06bf15..30670902e62 100644 --- a/meson.options +++ b/meson.options @@ -1,13 +1,22 @@ # vim: filetype=meson -option('doc-gen', type : 'boolean', value : false, +option( + 'doc-gen', + type : 'boolean', + value : false, description : 'Generate documentation', ) -option('unit-tests', type : 'boolean', value : true, +option( + 'unit-tests', + type : 'boolean', + value : true, description : 'Build unit tests', ) -option('bindings', type : 'boolean', value : true, +option( + 'bindings', + type : 'boolean', + value : true, description : 'Build language bindings (e.g. Perl)', ) diff --git a/misc/launchd/meson.build b/misc/launchd/meson.build new file mode 100644 index 00000000000..5168131d198 --- /dev/null +++ b/misc/launchd/meson.build @@ -0,0 +1,13 @@ +configure_file( + input : 'org.nixos.nix-daemon.plist.in', + output : 'org.nixos.nix-daemon.plist', + install : true, + install_dir : get_option('prefix') / 'Library/LaunchDaemons', + install_mode : 'rw-r--r--', + configuration : { + # TODO: unhardcode paths with something like: + # 'storedir' : store_dir, + # 'localstatedir' : localstatedir, + # 'bindir' : bindir, + }, +) diff --git a/misc/meson.build b/misc/meson.build index a6d1f944bbc..82f2b0c6571 100644 --- a/misc/meson.build +++ b/misc/meson.build @@ -2,4 +2,10 @@ subdir('bash') subdir('fish') subdir('zsh') -subdir('systemd') +if host_machine.system() == 'linux' + subdir('systemd') +endif + +if host_machine.system() == 'darwin' + subdir('launchd') +endif diff --git a/misc/systemd/nix-daemon.conf.in b/misc/systemd/nix-daemon.conf.in index e7b264234ab..a0ddc401918 100644 --- a/misc/systemd/nix-daemon.conf.in +++ b/misc/systemd/nix-daemon.conf.in @@ -1 +1,2 @@ -d @localstatedir@/nix/daemon-socket 0755 root root - - +d @localstatedir@/nix/daemon-socket 0755 root root - - +d @localstatedir@/nix/builds 0755 root root 7d - diff --git a/nix-meson-build-support/big-objs/meson.build b/nix-meson-build-support/big-objs/meson.build new file mode 100644 index 00000000000..7e422abd86e --- /dev/null +++ b/nix-meson-build-support/big-objs/meson.build @@ -0,0 +1,6 @@ +if host_machine.system() == 'windows' + # libexpr's primops creates a large object + # Without the following flag, we'll get errors when cross-compiling to mingw32: + # Fatal error: can't write 66 bytes to section .text of src/libexpr/libnixexpr.dll.p/primops.cc.obj: 'file too big' + add_project_arguments([ '-Wa,-mbig-obj' ], language: 'cpp') +endif diff --git a/build-utils-meson/common/meson.build b/nix-meson-build-support/common/meson.build similarity index 83% rename from build-utils-meson/common/meson.build rename to nix-meson-build-support/common/meson.build index f0322183e8a..b9140256a2f 100644 --- a/build-utils-meson/common/meson.build +++ b/nix-meson-build-support/common/meson.build @@ -10,13 +10,11 @@ add_project_arguments( '-Werror=suggest-override', '-Werror=switch', '-Werror=switch-enum', + '-Werror=undef', '-Werror=unused-result', + '-Werror=sign-compare', '-Wignored-qualifiers', '-Wimplicit-fallthrough', '-Wno-deprecated-declarations', language : 'cpp', ) - -if get_option('buildtype') not in ['debug'] - add_project_arguments('-O3', language : 'cpp') -endif diff --git a/build-utils-meson/deps-lists/meson.build b/nix-meson-build-support/deps-lists/meson.build similarity index 100% rename from build-utils-meson/deps-lists/meson.build rename to nix-meson-build-support/deps-lists/meson.build diff --git a/build-utils-meson/export-all-symbols/meson.build b/nix-meson-build-support/export-all-symbols/meson.build similarity index 100% rename from build-utils-meson/export-all-symbols/meson.build rename to nix-meson-build-support/export-all-symbols/meson.build diff --git a/build-utils-meson/export/meson.build b/nix-meson-build-support/export/meson.build similarity index 87% rename from build-utils-meson/export/meson.build rename to nix-meson-build-support/export/meson.build index 9f59505721e..950bd954434 100644 --- a/build-utils-meson/export/meson.build +++ b/nix-meson-build-support/export/meson.build @@ -11,13 +11,18 @@ endforeach requires_public += deps_public extra_pkg_config_variables = get_variable('extra_pkg_config_variables', {}) + +extra_cflags = [] +if not meson.project_name().endswith('-c') + extra_cflags += ['-std=c++2a'] +endif + import('pkgconfig').generate( this_library, filebase : meson.project_name(), name : 'Nix', description : 'Nix Package Manager', - subdirs : ['nix'], - extra_cflags : ['-std=c++2a'], + extra_cflags : extra_cflags, requires : requires_public, requires_private : requires_private, libraries_private : libraries_private, diff --git a/build-utils-meson/generate-header/meson.build b/nix-meson-build-support/generate-header/meson.build similarity index 100% rename from build-utils-meson/generate-header/meson.build rename to nix-meson-build-support/generate-header/meson.build diff --git a/build-utils-meson/libatomic/meson.build b/nix-meson-build-support/libatomic/meson.build similarity index 100% rename from build-utils-meson/libatomic/meson.build rename to nix-meson-build-support/libatomic/meson.build diff --git a/build-utils-meson/subprojects/meson.build b/nix-meson-build-support/subprojects/meson.build similarity index 100% rename from build-utils-meson/subprojects/meson.build rename to nix-meson-build-support/subprojects/meson.build diff --git a/build-utils-meson/windows-version/meson.build b/nix-meson-build-support/windows-version/meson.build similarity index 77% rename from build-utils-meson/windows-version/meson.build rename to nix-meson-build-support/windows-version/meson.build index 3a008e5df94..ed4caaa9a5c 100644 --- a/build-utils-meson/windows-version/meson.build +++ b/nix-meson-build-support/windows-version/meson.build @@ -2,5 +2,5 @@ if host_machine.system() == 'windows' # https://learn.microsoft.com/en-us/cpp/porting/modifying-winver-and-win32-winnt?view=msvc-170 # #define _WIN32_WINNT_WIN8 0x0602 # We currently don't use any API which requires higher than this. - add_project_arguments([ '-D_WIN32_WINNT=0x0602' ], language: 'cpp') + add_project_arguments([ '-D_WIN32_WINNT=0x0602' ], language : 'cpp') endif diff --git a/scripts/binary-tarball.nix b/packaging/binary-tarball.nix similarity index 81% rename from scripts/binary-tarball.nix rename to packaging/binary-tarball.nix index 671c8e96e38..2050384b03f 100644 --- a/scripts/binary-tarball.nix +++ b/packaging/binary-tarball.nix @@ -1,14 +1,18 @@ -{ runCommand -, system -, buildPackages -, cacert -, nix +{ + runCommand, + system, + buildPackages, + cacert, + nix, }: let installerClosureInfo = buildPackages.closureInfo { - rootPaths = [ nix cacert ]; + rootPaths = [ + nix + cacert + ]; }; inherit (nix) version; @@ -22,18 +26,18 @@ in runCommand "nix-binary-tarball-${version}" env '' cp ${installerClosureInfo}/registration $TMPDIR/reginfo - cp ${./create-darwin-volume.sh} $TMPDIR/create-darwin-volume.sh - substitute ${./install-nix-from-tarball.sh} $TMPDIR/install \ + cp ${../scripts/create-darwin-volume.sh} $TMPDIR/create-darwin-volume.sh + substitute ${../scripts/install-nix-from-tarball.sh} $TMPDIR/install \ --subst-var-by nix ${nix} \ --subst-var-by cacert ${cacert} - substitute ${./install-darwin-multi-user.sh} $TMPDIR/install-darwin-multi-user.sh \ + substitute ${../scripts/install-darwin-multi-user.sh} $TMPDIR/install-darwin-multi-user.sh \ --subst-var-by nix ${nix} \ --subst-var-by cacert ${cacert} - substitute ${./install-systemd-multi-user.sh} $TMPDIR/install-systemd-multi-user.sh \ + substitute ${../scripts/install-systemd-multi-user.sh} $TMPDIR/install-systemd-multi-user.sh \ --subst-var-by nix ${nix} \ --subst-var-by cacert ${cacert} - substitute ${./install-multi-user.sh} $TMPDIR/install-multi-user \ + substitute ${../scripts/install-multi-user.sh} $TMPDIR/install-multi-user \ --subst-var-by nix ${nix} \ --subst-var-by cacert ${cacert} @@ -65,7 +69,7 @@ runCommand "nix-binary-tarball-${version}" env '' fn=$out/$dir.tar.xz mkdir -p $out/nix-support echo "file binary-dist $fn" >> $out/nix-support/hydra-build-products - tar cvfJ $fn \ + tar cfJ $fn \ --owner=0 --group=0 --mode=u+rw,uga+r \ --mtime='1970-01-01' \ --absolute-names \ diff --git a/packaging/components.nix b/packaging/components.nix index e1f661be8fb..b40bd45b0e4 100644 --- a/packaging/components.nix +++ b/packaging/components.nix @@ -1,29 +1,341 @@ { lib, + pkgs, src, officialRelease, + maintainers, }: scope: let - inherit (scope) callPackage; + inherit (scope) + callPackage + ; + inherit + (scope.callPackage ( + { stdenv }: + { + inherit stdenv; + } + ) { }) + stdenv + ; + inherit (pkgs.buildPackages) + meson + ninja + pkg-config + ; baseVersion = lib.fileContents ../.version; versionSuffix = lib.optionalString (!officialRelease) "pre"; - fineVersionSuffix = lib.optionalString - (!officialRelease) - "pre${builtins.substring 0 8 (src.lastModifiedDate or src.lastModified or "19700101")}_${src.shortRev or "dirty"}"; + fineVersionSuffix = + lib.optionalString (!officialRelease) + "pre${ + builtins.substring 0 8 (src.lastModifiedDate or src.lastModified or "19700101") + }_${src.shortRev or "dirty"}"; fineVersion = baseVersion + fineVersionSuffix; + + root = ../.; + + # Indirection for Nixpkgs to override when package.nix files are vendored + filesetToSource = lib.fileset.toSource; + + /** + Given a set of layers, create a mkDerivation-like function + */ + mkPackageBuilder = + exts: userFn: stdenv.mkDerivation (lib.extends (lib.composeManyExtensions exts) userFn); + + setVersionLayer = finalAttrs: prevAttrs: { + preConfigure = + prevAttrs.preConfigure or "" + + + # Update the repo-global .version file. + # Symlink ./.version points there, but by default only workDir is writable. + '' + chmod u+w ./.version + echo ${finalAttrs.version} > ./.version + ''; + }; + + localSourceLayer = + finalAttrs: prevAttrs: + let + workDirPath = + # Ideally we'd pick finalAttrs.workDir, but for now `mkDerivation` has + # the requirement that everything except passthru and meta must be + # serialized by mkDerivation, which doesn't work for this. + prevAttrs.workDir; + + workDirSubpath = lib.path.removePrefix root workDirPath; + sources = + assert prevAttrs.fileset._type == "fileset"; + prevAttrs.fileset; + src = lib.fileset.toSource { + fileset = sources; + inherit root; + }; + + in + { + sourceRoot = "${src.name}/" + workDirSubpath; + inherit src; + + # Clear what `derivation` can't/shouldn't serialize; see prevAttrs.workDir. + fileset = null; + workDir = null; + }; + + resolveRelPath = p: lib.path.removePrefix root p; + + makeFetchedSourceLayer = + finalScope: finalAttrs: prevAttrs: + let + workDirPath = + # Ideally we'd pick finalAttrs.workDir, but for now `mkDerivation` has + # the requirement that everything except passthru and meta must be + # serialized by mkDerivation, which doesn't work for this. + prevAttrs.workDir; + + workDirSubpath = resolveRelPath workDirPath; + + in + { + sourceRoot = "${finalScope.patchedSrc.name}/" + workDirSubpath; + src = finalScope.patchedSrc; + version = + let + n = lib.length finalScope.patches; + in + if n == 0 then prevAttrs.version else prevAttrs.version + "+${toString n}"; + + # Clear what `derivation` can't/shouldn't serialize; see prevAttrs.workDir. + fileset = null; + workDir = null; + }; + + mesonLayer = finalAttrs: prevAttrs: { + # NOTE: + # As of https://github.com/NixOS/nixpkgs/blob/8baf8241cea0c7b30e0b8ae73474cb3de83c1a30/pkgs/by-name/me/meson/setup-hook.sh#L26, + # `mesonBuildType` defaults to `plain` if not specified. We want our Nix-built binaries to be optimized by default. + # More on build types here: https://mesonbuild.com/Builtin-options.html#details-for-buildtype. + mesonBuildType = "release"; + # NOTE: + # Users who are debugging Nix builds are expected to set the environment variable `mesonBuildType`, per the + # guidance in https://github.com/NixOS/nix/blob/8a3fc27f1b63a08ac983ee46435a56cf49ebaf4a/doc/manual/source/development/debugging.md?plain=1#L10. + # For this reason, we don't want to refer to `finalAttrs.mesonBuildType` here, but rather use the environment variable. + preConfigure = + prevAttrs.preConfigure or "" + + + lib.optionalString + ( + !stdenv.hostPlatform.isWindows + # build failure + && !stdenv.hostPlatform.isStatic + # LTO breaks exception handling on x86-64-darwin. + && stdenv.system != "x86_64-darwin" + ) + '' + case "$mesonBuildType" in + release|minsize) appendToVar mesonFlags "-Db_lto=true" ;; + *) appendToVar mesonFlags "-Db_lto=false" ;; + esac + ''; + nativeBuildInputs = [ + meson + ninja + ] ++ prevAttrs.nativeBuildInputs or [ ]; + mesonCheckFlags = prevAttrs.mesonCheckFlags or [ ] ++ [ + "--print-errorlogs" + ]; + }; + + mesonBuildLayer = finalAttrs: prevAttrs: { + nativeBuildInputs = prevAttrs.nativeBuildInputs or [ ] ++ [ + pkg-config + ]; + separateDebugInfo = !stdenv.hostPlatform.isStatic; + hardeningDisable = lib.optional stdenv.hostPlatform.isStatic "pie"; + }; + + mesonLibraryLayer = finalAttrs: prevAttrs: { + outputs = prevAttrs.outputs or [ "out" ] ++ [ "dev" ]; + }; + + fixupStaticLayer = finalAttrs: prevAttrs: { + postFixup = + prevAttrs.postFixup or "" + + lib.optionalString (stdenv.hostPlatform.isStatic) '' + # HACK: Otherwise the result will have the entire buildInputs closure + # injected by the pkgsStatic stdenv + # + rm -f $out/nix-support/propagated-build-inputs + ''; + }; + + # Work around weird `--as-needed` linker behavior with BSD, see + # https://github.com/mesonbuild/meson/issues/3593 + bsdNoLinkAsNeeded = + finalAttrs: prevAttrs: + lib.optionalAttrs stdenv.hostPlatform.isBSD { + mesonFlags = [ (lib.mesonBool "b_asneeded" false) ] ++ prevAttrs.mesonFlags or [ ]; + }; + + nixDefaultsLayer = finalAttrs: prevAttrs: { + strictDeps = prevAttrs.strictDeps or true; + enableParallelBuilding = true; + pos = builtins.unsafeGetAttrPos "pname" prevAttrs; + meta = prevAttrs.meta or { } // { + homepage = prevAttrs.meta.homepage or "https://nixos.org/nix"; + longDescription = + prevAttrs.longDescription or '' + Nix is a powerful package manager for mainly Linux and other Unix systems that + makes package management reliable and reproducible. It provides atomic + upgrades and rollbacks, side-by-side installation of multiple versions of + a package, multi-user package management and easy setup of build + environments. + ''; + license = prevAttrs.meta.license or lib.licenses.lgpl21Plus; + maintainers = prevAttrs.meta.maintainers or [ ] ++ scope.maintainers; + platforms = prevAttrs.meta.platforms or (lib.platforms.unix ++ lib.platforms.windows); + }; + }; + + /** + Append patches to the source layer. + */ + appendPatches = + scope: patches: + scope.overrideScope ( + finalScope: prevScope: { + patches = prevScope.patches ++ patches; + } + ); + in # This becomes the pkgs.nixComponents attribute set { version = baseVersion + versionSuffix; inherit versionSuffix; + inherit officialRelease; + inherit maintainers; + + inherit filesetToSource; + + /** + A user-provided extension function to apply to each component derivation. + */ + mesonComponentOverrides = finalAttrs: prevAttrs: { }; + + /** + An overridable derivation layer for handling the sources. + */ + sourceLayer = localSourceLayer; + + /** + Resolve a path value to either itself or a path in the `src`, depending + whether `overrideSource` was called. + */ + resolvePath = p: p; + + /** + Apply an extension function (i.e. overlay-shaped) to all component derivations. + */ + overrideAllMesonComponents = + f: + scope.overrideScope ( + finalScope: prevScope: { + mesonComponentOverrides = lib.composeExtensions scope.mesonComponentOverrides f; + } + ); + + /** + Provide an alternate source. This allows the expressions to be vendored without copying the sources, + but it does make the build non-granular; all components will use a complete source. + + Packaging expressions will be ignored. + + Single argument: the source to use. + + See also `appendPatches` + */ + overrideSource = + src: + scope.overrideScope ( + finalScope: prevScope: { + sourceLayer = makeFetchedSourceLayer finalScope; + /** + Unpatched source for the build of Nix. Packaging expressions will be ignored. + */ + src = src; + /** + Patches for the whole Nix source. Changes to packaging expressions will be ignored. + */ + patches = [ ]; + /** + Fetched and patched source to be used in component derivations. + */ + patchedSrc = + if finalScope.patches == [ ] then + src + else + pkgs.buildPackages.srcOnly ( + pkgs.buildPackages.stdenvNoCC.mkDerivation { + name = "${finalScope.src.name or "nix-source"}-patched"; + inherit (finalScope) src patches; + } + ); + resolvePath = p: finalScope.patchedSrc + "/${resolveRelPath p}"; + filesetToSource = { root, fileset }: finalScope.resolvePath root; + appendPatches = appendPatches finalScope; + } + ); + + /** + Append patches to be applied to the whole Nix source. + This affects all components. + + Changes to the packaging expressions will be ignored. + */ + appendPatches = + patches: + # switch to "fetched" source first, so that patches apply to the whole tree. + (scope.overrideSource "${./..}").appendPatches patches; + + mkMesonDerivation = mkPackageBuilder [ + nixDefaultsLayer + scope.sourceLayer + setVersionLayer + mesonLayer + fixupStaticLayer + scope.mesonComponentOverrides + ]; + mkMesonExecutable = mkPackageBuilder [ + nixDefaultsLayer + bsdNoLinkAsNeeded + scope.sourceLayer + setVersionLayer + mesonLayer + mesonBuildLayer + fixupStaticLayer + scope.mesonComponentOverrides + ]; + mkMesonLibrary = mkPackageBuilder [ + nixDefaultsLayer + bsdNoLinkAsNeeded + scope.sourceLayer + mesonLayer + setVersionLayer + mesonBuildLayer + mesonLibraryLayer + fixupStaticLayer + scope.mesonComponentOverrides + ]; nix-util = callPackage ../src/libutil/package.nix { }; nix-util-c = callPackage ../src/libutil-c/package.nix { }; @@ -36,6 +348,7 @@ in nix-store-tests = callPackage ../src/libstore-tests/package.nix { }; nix-fetchers = callPackage ../src/libfetchers/package.nix { }; + nix-fetchers-c = callPackage ../src/libfetchers-c/package.nix { }; nix-fetchers-tests = callPackage ../src/libfetchers-tests/package.nix { }; nix-expr = callPackage ../src/libexpr/package.nix { }; @@ -54,7 +367,9 @@ in nix-cli = callPackage ../src/nix/package.nix { version = fineVersion; }; - nix-functional-tests = callPackage ../src/nix-functional-tests/package.nix { version = fineVersion; }; + nix-functional-tests = callPackage ../tests/functional/package.nix { + version = fineVersion; + }; nix-manual = callPackage ../doc/manual/package.nix { version = fineVersion; }; nix-internal-api-docs = callPackage ../src/internal-api-docs/package.nix { version = fineVersion; }; @@ -62,5 +377,57 @@ in nix-perl-bindings = callPackage ../src/perl/package.nix { }; - nix-everything = callPackage ../packaging/everything.nix { }; + nix-everything = callPackage ../packaging/everything.nix { } // { + # Note: no `passthru.overrideAllMesonComponents` etc + # This would propagate into `nix.overrideAttrs f`, but then discard + # `f` when `.overrideAllMesonComponents` is used. + # Both "methods" should be views on the same fixpoint overriding mechanism + # for that to work. For now, we intentionally don't support the broken + # two-fixpoint solution. + /** + Apply an extension function (i.e. overlay-shaped) to all component derivations, and return the nix package. + + Single argument: the extension function to apply (finalAttrs: prevAttrs: { ... }) + */ + overrideAllMesonComponents = f: (scope.overrideAllMesonComponents f).nix-everything; + + /** + Append patches to be applied to the whole Nix source. + This affects all components. + + Changes to the packaging expressions will be ignored. + + Single argument: list of patches to apply + + See also `overrideSource` + */ + appendPatches = ps: (scope.appendPatches ps).nix-everything; + + /** + Provide an alternate source. This allows the expressions to be vendored without copying the sources, + but it does make the build non-granular; all components will use a complete source. + + Packaging expressions will be ignored. + + Filesets in the packaging expressions will be ignored. + + Single argument: the source to use. + + See also `appendPatches` + */ + overrideSource = src: (scope.overrideSource src).nix-everything; + + /** + Override any internals of the Nix package set. + + Single argument: the extension function to apply to the package set (finalScope: prevScope: { ... }) + + Example: + ``` + overrideScope (finalScope: prevScope: { aws-sdk-cpp = null; }) + ``` + */ + overrideScope = f: (scope.overrideScope f).nix-everything; + + }; } diff --git a/packaging/dependencies.nix b/packaging/dependencies.nix index a8005ce16c9..7ce3bf1259c 100644 --- a/packaging/dependencies.nix +++ b/packaging/dependencies.nix @@ -17,11 +17,7 @@ in let inherit (pkgs) lib; - root = ../.; - - stdenv = if prevStdenv.isDarwin && prevStdenv.isx86_64 - then darwinStdenv - else prevStdenv; + stdenv = if prevStdenv.isDarwin && prevStdenv.isx86_64 then darwinStdenv else prevStdenv; # Fix the following error with the default x86_64-darwin SDK: # @@ -30,198 +26,82 @@ let # Despite the use of the 10.13 deployment target here, the aligned # allocation function Clang uses with this setting actually works # all the way back to 10.6. + # NOTE: this is not just a version constraint, but a request to make Darwin + # provide this version level of support. Removing this minimum version + # request will regress the above error. darwinStdenv = pkgs.overrideSDK prevStdenv { darwinMinVersion = "10.13"; }; - # Nixpkgs implements this by returning a subpath into the fetched Nix sources. - resolvePath = p: p; - - # Indirection for Nixpkgs to override when package.nix files are vendored - filesetToSource = lib.fileset.toSource; - - /** Given a set of layers, create a mkDerivation-like function */ - mkPackageBuilder = exts: userFn: - stdenv.mkDerivation (lib.extends (lib.composeManyExtensions exts) userFn); - - localSourceLayer = finalAttrs: prevAttrs: - let - workDirPath = - # Ideally we'd pick finalAttrs.workDir, but for now `mkDerivation` has - # the requirement that everything except passthru and meta must be - # serialized by mkDerivation, which doesn't work for this. - prevAttrs.workDir; - - workDirSubpath = lib.path.removePrefix root workDirPath; - sources = assert prevAttrs.fileset._type == "fileset"; prevAttrs.fileset; - src = lib.fileset.toSource { fileset = sources; inherit root; }; - - in - { - sourceRoot = "${src.name}/" + workDirSubpath; - inherit src; - - # Clear what `derivation` can't/shouldn't serialize; see prevAttrs.workDir. - fileset = null; - workDir = null; - }; - - mesonLayer = finalAttrs: prevAttrs: - { - nativeBuildInputs = [ - pkgs.buildPackages.meson - pkgs.buildPackages.ninja - ] ++ prevAttrs.nativeBuildInputs or []; - mesonCheckFlags = prevAttrs.mesonCheckFlags or [] ++ [ - "--print-errorlogs" - ]; - }; - - mesonBuildLayer = finalAttrs: prevAttrs: - { - nativeBuildInputs = prevAttrs.nativeBuildInputs or [] ++ [ - pkgs.buildPackages.pkg-config - ]; - separateDebugInfo = !stdenv.hostPlatform.isStatic; - hardeningDisable = lib.optional stdenv.hostPlatform.isStatic "pie"; - }; - - mesonLibraryLayer = finalAttrs: prevAttrs: - { - outputs = prevAttrs.outputs or [ "out" ] ++ [ "dev" ]; - }; - - # Work around weird `--as-needed` linker behavior with BSD, see - # https://github.com/mesonbuild/meson/issues/3593 - bsdNoLinkAsNeeded = finalAttrs: prevAttrs: - lib.optionalAttrs stdenv.hostPlatform.isBSD { - mesonFlags = [ (lib.mesonBool "b_asneeded" false) ] ++ prevAttrs.mesonFlags or []; - }; - - miscGoodPractice = finalAttrs: prevAttrs: - { - strictDeps = prevAttrs.strictDeps or true; - enableParallelBuilding = true; - }; in scope: { inherit stdenv; - aws-sdk-cpp = (pkgs.aws-sdk-cpp.override { - apis = [ "s3" "transfer" ]; - customMemoryManagement = false; - }).overrideAttrs { - # only a stripped down version is built, which takes a lot less resources - # to build, so we don't need a "big-parallel" machine. - requiredSystemFeatures = [ ]; - }; - - libseccomp = pkgs.libseccomp.overrideAttrs (_: rec { - version = "2.5.5"; - src = pkgs.fetchurl { - url = "https://github.com/seccomp/libseccomp/releases/download/v${version}/libseccomp-${version}.tar.gz"; - hash = "sha256-JIosik2bmFiqa69ScSw0r+/PnJ6Ut23OAsHJqiX7M3U="; - }; - }); + aws-sdk-cpp = + (pkgs.aws-sdk-cpp.override { + apis = [ + "identity-management" + "s3" + "transfer" + ]; + customMemoryManagement = false; + }).overrideAttrs + { + # only a stripped down version is built, which takes a lot less resources + # to build, so we don't need a "big-parallel" machine. + requiredSystemFeatures = [ ]; + }; boehmgc = pkgs.boehmgc.override { enableLargeConfig = true; }; # TODO Hack until https://github.com/NixOS/nixpkgs/issues/45462 is fixed. - boost = (pkgs.boost.override { - extraB2Args = [ - "--with-container" - "--with-context" - "--with-coroutine" - ]; - }).overrideAttrs (old: { - # Need to remove `--with-*` to use `--with-libraries=...` - buildPhase = lib.replaceStrings [ "--without-python" ] [ "" ] old.buildPhase; - installPhase = lib.replaceStrings [ "--without-python" ] [ "" ] old.installPhase; - }); - - libgit2 = pkgs.libgit2.overrideAttrs (attrs: { - src = inputs.libgit2; - version = inputs.libgit2.lastModifiedDate; - cmakeFlags = attrs.cmakeFlags or [] - ++ [ "-DUSE_SSH=exec" ]; - nativeBuildInputs = attrs.nativeBuildInputs or [] - # gitMinimal does not build on Windows. See packbuilder patch. - ++ lib.optionals (!stdenv.hostPlatform.isWindows) [ - # Needed for `git apply`; see `prePatch` - pkgs.buildPackages.gitMinimal - ]; - # Only `git apply` can handle git binary patches - prePatch = attrs.prePatch or "" - + lib.optionalString (!stdenv.hostPlatform.isWindows) '' - patch() { - git apply - } - ''; - patches = attrs.patches or [] - ++ [ - ./patches/libgit2-mempack-thin-packfile.patch - ] - # gitMinimal does not build on Windows, but fortunately this patch only - # impacts interruptibility - ++ lib.optionals (!stdenv.hostPlatform.isWindows) [ - # binary patch; see `prePatch` - ./patches/libgit2-packbuilder-callback-interruptible.patch + boost = + (pkgs.boost.override { + extraB2Args = [ + "--with-container" + "--with-context" + "--with-coroutine" + "--with-iostreams" ]; - }); - - busybox-sandbox-shell = pkgs.busybox-sandbox-shell or (pkgs.busybox.override { - useMusl = true; - enableStatic = true; - enableMinimal = true; - extraConfig = '' - CONFIG_FEATURE_FANCY_ECHO y - CONFIG_FEATURE_SH_MATH y - CONFIG_FEATURE_SH_MATH_64 y - - CONFIG_ASH y - CONFIG_ASH_OPTIMIZE_FOR_SIZE y - - CONFIG_ASH_ALIAS y - CONFIG_ASH_BASH_COMPAT y - CONFIG_ASH_CMDCMD y - CONFIG_ASH_ECHO y - CONFIG_ASH_GETOPTS y - CONFIG_ASH_INTERNAL_GLOB y - CONFIG_ASH_JOB_CONTROL y - CONFIG_ASH_PRINTF y - CONFIG_ASH_TEST y - ''; - }); - - # TODO change in Nixpkgs, Windows works fine. First commit of - # https://github.com/NixOS/nixpkgs/pull/322977 backported will fix. - toml11 = pkgs.toml11.overrideAttrs (old: { - meta.platforms = lib.platforms.all; - }); - - inherit resolvePath filesetToSource; - - mkMesonDerivation = - mkPackageBuilder [ - miscGoodPractice - localSourceLayer - mesonLayer - ]; - mkMesonExecutable = - mkPackageBuilder [ - miscGoodPractice - bsdNoLinkAsNeeded - localSourceLayer - mesonLayer - mesonBuildLayer - ]; - mkMesonLibrary = - mkPackageBuilder [ - miscGoodPractice - bsdNoLinkAsNeeded - localSourceLayer - mesonLayer - mesonBuildLayer - mesonLibraryLayer - ]; + enableIcu = false; + }).overrideAttrs + (old: { + # Need to remove `--with-*` to use `--with-libraries=...` + buildPhase = lib.replaceStrings [ "--without-python" ] [ "" ] old.buildPhase; + installPhase = lib.replaceStrings [ "--without-python" ] [ "" ] old.installPhase; + }); + + libgit2 = + if lib.versionAtLeast pkgs.libgit2.version "1.9.0" then + pkgs.libgit2 + else + pkgs.libgit2.overrideAttrs (attrs: { + # libgit2: Nixpkgs 24.11 has < 1.9.0, which needs our patches + nativeBuildInputs = + attrs.nativeBuildInputs or [ ] + # gitMinimal does not build on Windows. See packbuilder patch. + ++ lib.optionals (!stdenv.hostPlatform.isWindows) [ + # Needed for `git apply`; see `prePatch` + pkgs.buildPackages.gitMinimal + ]; + # Only `git apply` can handle git binary patches + prePatch = + attrs.prePatch or "" + + lib.optionalString (!stdenv.hostPlatform.isWindows) '' + patch() { + git apply + } + ''; + patches = + attrs.patches or [ ] + ++ [ + ./patches/libgit2-mempack-thin-packfile.patch + ] + # gitMinimal does not build on Windows, but fortunately this patch only + # impacts interruptibility + ++ lib.optionals (!stdenv.hostPlatform.isWindows) [ + # binary patch; see `prePatch` + ./patches/libgit2-packbuilder-callback-interruptible.patch + ]; + }); } diff --git a/packaging/dev-shell.nix b/packaging/dev-shell.nix index 30ac518d5f7..8d3fa38527a 100644 --- a/packaging/dev-shell.nix +++ b/packaging/dev-shell.nix @@ -1,128 +1,139 @@ -{ lib, devFlake }: +{ + lib, + devFlake, +}: { pkgs }: -pkgs.nixComponents.nix-util.overrideAttrs (attrs: - -let - stdenv = pkgs.nixDependencies.stdenv; - buildCanExecuteHost = stdenv.buildPlatform.canExecute stdenv.hostPlatform; - modular = devFlake.getSystem stdenv.buildPlatform.system; - transformFlag = prefix: flag: - assert builtins.isString flag; - let - rest = builtins.substring 2 (builtins.stringLength flag) flag; - in +pkgs.nixComponents2.nix-util.overrideAttrs ( + attrs: + + let + stdenv = pkgs.nixDependencies2.stdenv; + buildCanExecuteHost = stdenv.buildPlatform.canExecute stdenv.hostPlatform; + modular = devFlake.getSystem stdenv.buildPlatform.system; + transformFlag = + prefix: flag: + assert builtins.isString flag; + let + rest = builtins.substring 2 (builtins.stringLength flag) flag; + in "-D${prefix}:${rest}"; - havePerl = stdenv.buildPlatform == stdenv.hostPlatform && stdenv.hostPlatform.isUnix; - ignoreCrossFile = flags: builtins.filter (flag: !(lib.strings.hasInfix "cross-file" flag)) flags; -in { - pname = "shell-for-" + attrs.pname; - - # Remove the version suffix to avoid unnecessary attempts to substitute in nix develop - version = lib.fileContents ../.version; - name = attrs.pname; - - installFlags = "sysconfdir=$(out)/etc"; - shellHook = '' - PATH=$prefix/bin:$PATH - unset PYTHONPATH - export MANPATH=$out/share/man:$MANPATH - - # Make bash completion work. - XDG_DATA_DIRS+=:$out/share - - # Make the default phases do the right thing. - # FIXME: this wouldn't be needed if the ninja package set buildPhase() instead of $buildPhase. - # FIXME: mesonConfigurePhase shouldn't cd to the build directory. It would be better to pass '-C ' to ninja. - - cdToBuildDir() { - if [[ ! -e build.ninja ]]; then - cd build - fi - } - - configurePhase() { - mesonConfigurePhase - } - - buildPhase() { - cdToBuildDir - ninjaBuildPhase - } - - checkPhase() { - cdToBuildDir - mesonCheckPhase - } - - installPhase() { - cdToBuildDir - ninjaInstallPhase - } - ''; - - # We use this shell with the local checkout, not unpackPhase. - src = null; - - env = { - # Needed for Meson to find Boost. - # https://github.com/NixOS/nixpkgs/issues/86131. - BOOST_INCLUDEDIR = "${lib.getDev pkgs.nixDependencies.boost}/include"; - BOOST_LIBRARYDIR = "${lib.getLib pkgs.nixDependencies.boost}/lib"; - # For `make format`, to work without installing pre-commit - _NIX_PRE_COMMIT_HOOKS_CONFIG = - "${(pkgs.formats.yaml { }).generate "pre-commit-config.yaml" modular.pre-commit.settings.rawConfig}"; - }; - - mesonFlags = - map (transformFlag "libutil") (ignoreCrossFile pkgs.nixComponents.nix-util.mesonFlags) - ++ map (transformFlag "libstore") (ignoreCrossFile pkgs.nixComponents.nix-store.mesonFlags) - ++ map (transformFlag "libfetchers") (ignoreCrossFile pkgs.nixComponents.nix-fetchers.mesonFlags) - ++ lib.optionals havePerl (map (transformFlag "perl") (ignoreCrossFile pkgs.nixComponents.nix-perl-bindings.mesonFlags)) - ++ map (transformFlag "libexpr") (ignoreCrossFile pkgs.nixComponents.nix-expr.mesonFlags) - ++ map (transformFlag "libcmd") (ignoreCrossFile pkgs.nixComponents.nix-cmd.mesonFlags) - ; - - nativeBuildInputs = attrs.nativeBuildInputs or [] - ++ pkgs.nixComponents.nix-util.nativeBuildInputs - ++ pkgs.nixComponents.nix-store.nativeBuildInputs - ++ pkgs.nixComponents.nix-fetchers.nativeBuildInputs - ++ pkgs.nixComponents.nix-expr.nativeBuildInputs - ++ lib.optionals havePerl pkgs.nixComponents.nix-perl-bindings.nativeBuildInputs - ++ lib.optionals buildCanExecuteHost pkgs.nixComponents.nix-manual.externalNativeBuildInputs - ++ pkgs.nixComponents.nix-internal-api-docs.nativeBuildInputs - ++ pkgs.nixComponents.nix-external-api-docs.nativeBuildInputs - ++ pkgs.nixComponents.nix-functional-tests.externalNativeBuildInputs - ++ lib.optional - (!buildCanExecuteHost - # Hack around https://github.com/nixos/nixpkgs/commit/bf7ad8cfbfa102a90463433e2c5027573b462479 - && !(stdenv.hostPlatform.isWindows && stdenv.buildPlatform.isDarwin) - && stdenv.hostPlatform.emulatorAvailable pkgs.buildPackages - && lib.meta.availableOn stdenv.buildPlatform (stdenv.hostPlatform.emulator pkgs.buildPackages)) - pkgs.buildPackages.mesonEmulatorHook - ++ [ - pkgs.buildPackages.cmake - pkgs.buildPackages.shellcheck - pkgs.buildPackages.changelog-d - modular.pre-commit.settings.package - (pkgs.writeScriptBin "pre-commit-hooks-install" - modular.pre-commit.settings.installationScript) - ] - # TODO: Remove the darwin check once - # https://github.com/NixOS/nixpkgs/pull/291814 is available - ++ lib.optional (stdenv.cc.isClang && !stdenv.buildPlatform.isDarwin) pkgs.buildPackages.bear - ++ lib.optional (stdenv.cc.isClang && stdenv.hostPlatform == stdenv.buildPlatform) (lib.hiPrio pkgs.buildPackages.clang-tools); - - buildInputs = attrs.buildInputs or [] - ++ pkgs.nixComponents.nix-util.buildInputs - ++ pkgs.nixComponents.nix-store.buildInputs - ++ pkgs.nixComponents.nix-store-tests.externalBuildInputs - ++ pkgs.nixComponents.nix-fetchers.buildInputs - ++ pkgs.nixComponents.nix-expr.buildInputs - ++ pkgs.nixComponents.nix-expr.externalPropagatedBuildInputs - ++ pkgs.nixComponents.nix-cmd.buildInputs - ++ lib.optionals havePerl pkgs.nixComponents.nix-perl-bindings.externalBuildInputs - ++ lib.optional havePerl pkgs.perl - ; -}) + havePerl = stdenv.buildPlatform == stdenv.hostPlatform && stdenv.hostPlatform.isUnix; + ignoreCrossFile = flags: builtins.filter (flag: !(lib.strings.hasInfix "cross-file" flag)) flags; + in + { + pname = "shell-for-" + attrs.pname; + + # Remove the version suffix to avoid unnecessary attempts to substitute in nix develop + version = lib.fileContents ../.version; + name = attrs.pname; + + installFlags = "sysconfdir=$(out)/etc"; + shellHook = '' + PATH=$prefix/bin:$PATH + unset PYTHONPATH + export MANPATH=$out/share/man:$MANPATH + + # Make bash completion work. + XDG_DATA_DIRS+=:$out/share + + # Make the default phases do the right thing. + # FIXME: this wouldn't be needed if the ninja package set buildPhase() instead of $buildPhase. + # FIXME: mesonConfigurePhase shouldn't cd to the build directory. It would be better to pass '-C ' to ninja. + + cdToBuildDir() { + if [[ ! -e build.ninja ]]; then + cd build + fi + } + + configurePhase() { + mesonConfigurePhase + } + + buildPhase() { + cdToBuildDir + ninjaBuildPhase + } + + checkPhase() { + cdToBuildDir + mesonCheckPhase + } + + installPhase() { + cdToBuildDir + ninjaInstallPhase + } + ''; + + # We use this shell with the local checkout, not unpackPhase. + src = null; + + env = + { + # For `make format`, to work without installing pre-commit + _NIX_PRE_COMMIT_HOOKS_CONFIG = "${(pkgs.formats.yaml { }).generate "pre-commit-config.yaml" + modular.pre-commit.settings.rawConfig + }"; + } + // lib.optionalAttrs stdenv.hostPlatform.isLinux { + CC_LD = "mold"; + CXX_LD = "mold"; + }; + + mesonFlags = + map (transformFlag "libutil") (ignoreCrossFile pkgs.nixComponents2.nix-util.mesonFlags) + ++ map (transformFlag "libstore") (ignoreCrossFile pkgs.nixComponents2.nix-store.mesonFlags) + ++ map (transformFlag "libfetchers") (ignoreCrossFile pkgs.nixComponents2.nix-fetchers.mesonFlags) + ++ lib.optionals havePerl ( + map (transformFlag "perl") (ignoreCrossFile pkgs.nixComponents2.nix-perl-bindings.mesonFlags) + ) + ++ map (transformFlag "libexpr") (ignoreCrossFile pkgs.nixComponents2.nix-expr.mesonFlags) + ++ map (transformFlag "libcmd") (ignoreCrossFile pkgs.nixComponents2.nix-cmd.mesonFlags); + + nativeBuildInputs = + attrs.nativeBuildInputs or [ ] + ++ pkgs.nixComponents2.nix-util.nativeBuildInputs + ++ pkgs.nixComponents2.nix-store.nativeBuildInputs + ++ pkgs.nixComponents2.nix-fetchers.nativeBuildInputs + ++ pkgs.nixComponents2.nix-expr.nativeBuildInputs + ++ lib.optionals havePerl pkgs.nixComponents2.nix-perl-bindings.nativeBuildInputs + ++ lib.optionals buildCanExecuteHost pkgs.nixComponents2.nix-manual.externalNativeBuildInputs + ++ pkgs.nixComponents2.nix-internal-api-docs.nativeBuildInputs + ++ pkgs.nixComponents2.nix-external-api-docs.nativeBuildInputs + ++ pkgs.nixComponents2.nix-functional-tests.externalNativeBuildInputs + ++ lib.optional ( + !buildCanExecuteHost + # Hack around https://github.com/nixos/nixpkgs/commit/bf7ad8cfbfa102a90463433e2c5027573b462479 + && !(stdenv.hostPlatform.isWindows && stdenv.buildPlatform.isDarwin) + && stdenv.hostPlatform.emulatorAvailable pkgs.buildPackages + && lib.meta.availableOn stdenv.buildPlatform (stdenv.hostPlatform.emulator pkgs.buildPackages) + ) pkgs.buildPackages.mesonEmulatorHook + ++ [ + pkgs.buildPackages.cmake + pkgs.buildPackages.shellcheck + pkgs.buildPackages.changelog-d + modular.pre-commit.settings.package + (pkgs.writeScriptBin "pre-commit-hooks-install" modular.pre-commit.settings.installationScript) + pkgs.buildPackages.nixfmt-rfc-style + ] + ++ lib.optional (stdenv.cc.isClang && stdenv.hostPlatform == stdenv.buildPlatform) ( + lib.hiPrio pkgs.buildPackages.clang-tools + ) + ++ lib.optional stdenv.hostPlatform.isLinux pkgs.buildPackages.mold-wrapped; + + buildInputs = + attrs.buildInputs or [ ] + ++ pkgs.nixComponents2.nix-util.buildInputs + ++ pkgs.nixComponents2.nix-store.buildInputs + ++ pkgs.nixComponents2.nix-store-tests.externalBuildInputs + ++ pkgs.nixComponents2.nix-fetchers.buildInputs + ++ pkgs.nixComponents2.nix-expr.buildInputs + ++ pkgs.nixComponents2.nix-expr.externalPropagatedBuildInputs + ++ pkgs.nixComponents2.nix-cmd.buildInputs + ++ lib.optionals havePerl pkgs.nixComponents2.nix-perl-bindings.externalBuildInputs + ++ lib.optional havePerl pkgs.perl; + } +) diff --git a/packaging/everything.nix b/packaging/everything.nix index 0b04d2c6d93..5bf57f95a26 100644 --- a/packaging/everything.nix +++ b/packaging/everything.nix @@ -1,8 +1,11 @@ { lib, stdenv, + lndir, buildEnv, + maintainers, + nix-util, nix-util-c, nix-util-tests, @@ -12,6 +15,7 @@ nix-store-tests, nix-fetchers, + nix-fetchers-c, nix-fetchers-tests, nix-expr, @@ -38,66 +42,38 @@ nix-perl-bindings, testers, - runCommand, + + patchedSrc ? null, }: let - dev = stdenv.mkDerivation (finalAttrs: { - name = "nix-${nix-cli.version}-dev"; - pname = "nix"; - version = nix-cli.version; - dontUnpack = true; - dontBuild = true; - libs = map lib.getDev [ - nix-cmd - nix-expr - nix-expr-c - nix-fetchers - nix-flake - nix-flake-c - nix-main - nix-main-c - nix-store - nix-store-c - nix-util - nix-util-c - nix-perl-bindings - ]; - installPhase = '' - mkdir -p $out/nix-support - echo $libs >> $out/nix-support/propagated-build-inputs - ''; - passthru = { - tests = { - pkg-config = - testers.hasPkgConfigModules { - package = finalAttrs.finalPackage; - }; + libs = + { + inherit + nix-util + nix-util-c + nix-store + nix-store-c + nix-fetchers + nix-fetchers-c + nix-expr + nix-expr-c + nix-flake + nix-flake-c + nix-main + nix-main-c + nix-cmd + ; + } + // lib.optionalAttrs + (!stdenv.hostPlatform.isStatic && stdenv.buildPlatform.canExecute stdenv.hostPlatform) + { + # Currently fails in static build + inherit + nix-perl-bindings + ; }; - # If we were to fully emulate output selection here, we'd confuse the Nix CLIs, - # because they rely on `drvPath`. - dev = finalAttrs.finalPackage.out; - - libs = throw "`nix.dev.libs` is not meant to be used; use `nix.libs` instead."; - }; - meta = { - pkgConfigModules = [ - "nix-cmd" - "nix-expr" - "nix-expr-c" - "nix-fetchers" - "nix-flake" - "nix-flake-c" - "nix-main" - "nix-main-c" - "nix-store" - "nix-store-c" - "nix-util" - "nix-util-c" - ]; - }; - }); devdoc = buildEnv { name = "nix-${nix-cli.version}-devdoc"; paths = [ @@ -107,43 +83,100 @@ let }; in -(buildEnv { - name = "nix-${nix-cli.version}"; - paths = [ - nix-cli - nix-manual.man +stdenv.mkDerivation (finalAttrs: { + pname = "nix"; + version = nix-cli.version; + + /** + This package uses a multi-output derivation, even though some outputs could + have been provided directly by the constituent component that provides it. + + This is because not all tooling handles packages composed of arbitrary + outputs yet. This includes nix itself, https://github.com/NixOS/nix/issues/6507. + + `devdoc` is also available, but not listed here, because this attribute is + not an output of the same derivation that provides `out`, `dev`, etc. + */ + outputs = [ + "out" + "dev" + "doc" + "man" ]; - meta.mainProgram = "nix"; -}).overrideAttrs (finalAttrs: prevAttrs: { + /** + Unpacking is handled in this package's constituent components + */ + dontUnpack = true; + /** + Building is handled in this package's constituent components + */ + dontBuild = true; + + /** + `doCheck` controles whether tests are added as build gate for the combined package. + This includes both the unit tests and the functional tests, but not the + integration tests that run in CI (the flake's `hydraJobs` and some of the `checks`). + */ doCheck = true; - doInstallCheck = true; - - checkInputs = [ - # Make sure the unit tests have passed - nix-util-tests.tests.run - nix-store-tests.tests.run - nix-expr-tests.tests.run - nix-fetchers-tests.tests.run - nix-flake-tests.tests.run - - # dev bundle is ok - # (checkInputs must be empty paths??) - (runCommand "check-pkg-config" { checked = dev.tests.pkg-config; } "mkdir $out") - ] ++ - (if stdenv.buildPlatform.canExecute stdenv.hostPlatform - then [ - # TODO: add perl.tests - nix-perl-bindings + + /** + `fixupPhase` currently doesn't understand that a symlink output isn't writable. + + We don't compile or link anything in this derivation, so fixups aren't needed. + */ + dontFixup = true; + + checkInputs = + [ + # Make sure the unit tests have passed + nix-util-tests.tests.run + nix-store-tests.tests.run + nix-expr-tests.tests.run + nix-fetchers-tests.tests.run + nix-flake-tests.tests.run + + # Make sure the functional tests have passed + nix-functional-tests ] - else [ - nix-perl-bindings - ]); - installCheckInputs = [ - nix-functional-tests + ++ lib.optionals + (!stdenv.hostPlatform.isStatic && stdenv.buildPlatform.canExecute stdenv.hostPlatform) + [ + # Perl currently fails in static build + # TODO: Split out tests into a separate derivation? + nix-perl-bindings + ]; + + nativeBuildInputs = [ + lndir ]; - passthru = prevAttrs.passthru // { + + installPhase = + let + devPaths = lib.mapAttrsToList (_k: lib.getDev) finalAttrs.finalPackage.libs; + in + '' + mkdir -p $out $dev/nix-support + + # Custom files + echo $libs >> $dev/nix-support/propagated-build-inputs + echo ${nix-cli} ${lib.escapeShellArgs devPaths} >> $dev/nix-support/propagated-build-inputs + + # Merged outputs + lndir ${nix-cli} $out + + for lib in ${lib.escapeShellArgs devPaths}; do + lndir $lib $dev + done + + # Forwarded outputs + ln -sT ${nix-manual} $doc + ln -sT ${nix-manual.man} $man + ''; + + passthru = { inherit (nix-cli) version; + src = patchedSrc; /** These are the libraries that are part of the Nix project. They are used @@ -161,42 +194,54 @@ in unusedInputsForTests = [ nix ]; disallowedReferences = nix.all; ``` - */ - libs = { - inherit - nix-util - nix-util-c - nix-store - nix-store-c - nix-fetchers - nix-expr - nix-expr-c - nix-flake - nix-flake-c - nix-main - nix-main-c - ; - }; - - tests = prevAttrs.passthru.tests or {} // { - # TODO: create a proper fixpoint and: - # pkg-config = - # testers.hasPkgConfigModules { - # package = finalPackage; - # }; - }; + */ + inherit libs; /** - A derivation referencing the `dev` outputs of the Nix libraries. - */ - inherit dev; + Developer documentation for `nix`, in `share/doc/nix/{internal,external}-api/`. + + This is not a proper output; see `outputs` for context. + */ inherit devdoc; - doc = nix-manual; - outputs = [ "out" "dev" "devdoc" "doc" ]; - all = lib.attrValues (lib.genAttrs finalAttrs.passthru.outputs (outName: finalAttrs.finalPackage.${outName})); + + /** + Extra tests that test this package, but do not run as part of the build. + See + */ + tests = { + pkg-config = testers.hasPkgConfigModules { + package = finalAttrs.finalPackage; + }; + }; }; - meta = prevAttrs.meta // { + + meta = { + mainProgram = "nix"; description = "The Nix package manager"; - pkgConfigModules = dev.meta.pkgConfigModules; + longDescription = nix-cli.meta.longDescription; + homepage = nix-cli.meta.homepage; + license = nix-cli.meta.license; + maintainers = maintainers; + platforms = nix-cli.meta.platforms; + outputsToInstall = [ + "out" + "man" + ]; + pkgConfigModules = [ + "nix-cmd" + "nix-expr" + "nix-expr-c" + "nix-fetchers" + "nix-fetchers-c" + "nix-flake" + "nix-flake-c" + "nix-main" + "nix-main-c" + "nix-store" + "nix-store-c" + "nix-util" + "nix-util-c" + ]; }; + }) diff --git a/packaging/hydra.nix b/packaging/hydra.nix index 81406a2493c..27c09d9c9d4 100644 --- a/packaging/hydra.nix +++ b/packaging/hydra.nix @@ -1,115 +1,199 @@ -{ inputs -, binaryTarball -, forAllCrossSystems -, forAllSystems -, lib -, linux64BitSystems -, nixpkgsFor -, self -, officialRelease +{ + inputs, + forAllCrossSystems, + forAllSystems, + lib, + linux64BitSystems, + nixpkgsFor, + self, + officialRelease, }: let inherit (inputs) nixpkgs nixpkgs-regression; - installScriptFor = tarballs: - nixpkgsFor.x86_64-linux.native.callPackage ../scripts/installer.nix { + installScriptFor = + tarballs: + nixpkgsFor.x86_64-linux.native.callPackage ./installer { inherit tarballs; }; - testNixVersions = pkgs: daemon: - pkgs.nixComponents.nix-functional-tests.override { - pname = - "nix-tests" - + lib.optionalString - (lib.versionAtLeast daemon.version "2.4pre20211005" && - lib.versionAtLeast pkgs.nix.version "2.4pre20211005") - "-${pkgs.nix.version}-against-${daemon.version}"; + testNixVersions = + pkgs: daemon: + pkgs.nixComponents2.nix-functional-tests.override { + pname = "nix-daemon-compat-tests"; + version = "${pkgs.nix.version}-with-daemon-${daemon.version}"; test-daemon = daemon; }; - # Technically we could just return `pkgs.nixComponents`, but for Hydra it's + # Technically we could just return `pkgs.nixComponents2`, but for Hydra it's # convention to transpose it, and to transpose it efficiently, we need to # enumerate them manually, so that we don't evaluate unnecessary package sets. - forAllPackages = lib.genAttrs [ - "nix-everything" - "nix-util" - "nix-util-c" - "nix-util-test-support" - "nix-util-tests" - "nix-store" - "nix-store-c" - "nix-store-test-support" - "nix-store-tests" - "nix-fetchers" - "nix-fetchers-tests" - "nix-expr" - "nix-expr-c" - "nix-expr-test-support" - "nix-expr-tests" - "nix-flake" - "nix-flake-tests" - "nix-main" - "nix-main-c" - "nix-cmd" - "nix-cli" - "nix-functional-tests" - ]; + # See listingIsComplete below. + forAllPackages = forAllPackages' { }; + forAllPackages' = + { + enableBindings ? false, + enableDocs ? false, # already have separate attrs for these + }: + lib.genAttrs ( + [ + "nix-everything" + "nix-util" + "nix-util-c" + "nix-util-test-support" + "nix-util-tests" + "nix-store" + "nix-store-c" + "nix-store-test-support" + "nix-store-tests" + "nix-fetchers" + "nix-fetchers-c" + "nix-fetchers-tests" + "nix-expr" + "nix-expr-c" + "nix-expr-test-support" + "nix-expr-tests" + "nix-flake" + "nix-flake-c" + "nix-flake-tests" + "nix-main" + "nix-main-c" + "nix-cmd" + "nix-cli" + "nix-functional-tests" + ] + ++ lib.optionals enableBindings [ + "nix-perl-bindings" + ] + ++ lib.optionals enableDocs [ + "nix-manual" + "nix-internal-api-docs" + "nix-external-api-docs" + ] + ); in { + /** + An internal check to make sure our package listing is complete. + */ + listingIsComplete = + let + arbitrarySystem = "x86_64-linux"; + listedPkgs = forAllPackages' { + enableBindings = true; + enableDocs = true; + } (_: null); + actualPkgs = lib.concatMapAttrs ( + k: v: if lib.strings.hasPrefix "nix-" k then { ${k} = null; } else { } + ) nixpkgsFor.${arbitrarySystem}.native.nixComponents2; + diff = lib.concatStringsSep "\n" ( + lib.concatLists ( + lib.mapAttrsToList ( + k: _: + if (listedPkgs ? ${k}) && !(actualPkgs ? ${k}) then + [ "- ${k}: redundant?" ] + else if !(listedPkgs ? ${k}) && (actualPkgs ? ${k}) then + [ "- ${k}: missing?" ] + else + [ ] + ) (listedPkgs // actualPkgs) + ) + ); + in + if listedPkgs == actualPkgs then + { } + else + throw '' + Please update the components list in hydra.nix (or fix this check) + Differences: + ${diff} + ''; + # Binary package for various platforms. - build = forAllPackages (pkgName: - forAllSystems (system: nixpkgsFor.${system}.native.nixComponents.${pkgName})); + build = forAllPackages ( + pkgName: forAllSystems (system: nixpkgsFor.${system}.native.nixComponents2.${pkgName}) + ); - shellInputs = removeAttrs - (forAllSystems (system: self.devShells.${system}.default.inputDerivation)) - [ "i686-linux" ]; + shellInputs = removeAttrs (forAllSystems ( + system: self.devShells.${system}.default.inputDerivation + )) [ "i686-linux" ]; - buildStatic = forAllPackages (pkgName: - lib.genAttrs linux64BitSystems (system: nixpkgsFor.${system}.static.nixComponents.${pkgName})); + buildStatic = forAllPackages ( + pkgName: + lib.genAttrs linux64BitSystems ( + system: nixpkgsFor.${system}.native.pkgsStatic.nixComponents2.${pkgName} + ) + ); - buildCross = forAllPackages (pkgName: + buildCross = forAllPackages ( + pkgName: # Hack to avoid non-evaling package - (if pkgName == "nix-functional-tests" then lib.flip builtins.removeAttrs ["x86_64-w64-mingw32"] else lib.id) - (forAllCrossSystems (crossSystem: - lib.genAttrs [ "x86_64-linux" ] (system: nixpkgsFor.${system}.cross.${crossSystem}.nixComponents.${pkgName})))); - - buildNoGc = let - components = forAllSystems (system: - nixpkgsFor.${system}.native.nixComponents.overrideScope (self: super: { - nix-expr = super.nix-expr.override { enableGC = false; }; - }) - ); - in forAllPackages (pkgName: forAllSystems (system: components.${system}.${pkgName})); + ( + if pkgName == "nix-functional-tests" then + lib.flip builtins.removeAttrs [ "x86_64-w64-mingw32" ] + else + lib.id + ) + ( + forAllCrossSystems ( + crossSystem: + lib.genAttrs [ "x86_64-linux" ] ( + system: nixpkgsFor.${system}.cross.${crossSystem}.nixComponents2.${pkgName} + ) + ) + ) + ); - buildNoTests = forAllSystems (system: nixpkgsFor.${system}.native.nixComponents.nix-cli); + buildNoGc = + let + components = forAllSystems ( + system: + nixpkgsFor.${system}.native.nixComponents2.overrideScope ( + self: super: { + nix-expr = super.nix-expr.override { enableGC = false; }; + } + ) + ); + in + forAllPackages (pkgName: forAllSystems (system: components.${system}.${pkgName})); + + buildNoTests = forAllSystems (system: nixpkgsFor.${system}.native.nixComponents2.nix-cli); # Toggles some settings for better coverage. Windows needs these # library combinations, and Debian build Nix with GNU readline too. - buildReadlineNoMarkdown = let - components = forAllSystems (system: - nixpkgsFor.${system}.native.nixComponents.overrideScope (self: super: { - nix-cmd = super.nix-cmd.override { - enableMarkdown = false; - readlineFlavor = "readline"; - }; - }) - ); - in forAllPackages (pkgName: forAllSystems (system: components.${system}.${pkgName})); + buildReadlineNoMarkdown = + let + components = forAllSystems ( + system: + nixpkgsFor.${system}.native.nixComponents2.overrideScope ( + self: super: { + nix-cmd = super.nix-cmd.override { + enableMarkdown = false; + readlineFlavor = "readline"; + }; + } + ) + ); + in + forAllPackages (pkgName: forAllSystems (system: components.${system}.${pkgName})); # Perl bindings for various platforms. - perlBindings = forAllSystems (system: nixpkgsFor.${system}.native.nixComponents.nix-perl-bindings); + perlBindings = forAllSystems (system: nixpkgsFor.${system}.native.nixComponents2.nix-perl-bindings); # Binary tarball for various platforms, containing a Nix store # with the closure of 'nix' package, and the second half of # the installation script. - binaryTarball = forAllSystems (system: binaryTarball nixpkgsFor.${system}.native.nix nixpkgsFor.${system}.native); + binaryTarball = forAllSystems ( + system: nixpkgsFor.${system}.native.callPackage ./binary-tarball.nix { } + ); - binaryTarballCross = lib.genAttrs [ "x86_64-linux" ] (system: - forAllCrossSystems (crossSystem: - binaryTarball - nixpkgsFor.${system}.cross.${crossSystem}.nix - nixpkgsFor.${system}.cross.${crossSystem})); + binaryTarballCross = lib.genAttrs [ "x86_64-linux" ] ( + system: + forAllCrossSystems ( + crossSystem: nixpkgsFor.${system}.cross.${crossSystem}.callPackage ./binary-tarball.nix { } + ) + ); # The first half of the installation script. This is uploaded # to https://nixos.org/nix/install. It downloads the binary @@ -127,15 +211,13 @@ in self.hydraJobs.binaryTarballCross."x86_64-linux"."armv7l-unknown-linux-gnueabihf" self.hydraJobs.binaryTarballCross."x86_64-linux"."riscv64-unknown-linux-gnu" ]; - installerScriptForGHA = installScriptFor [ - # Native - self.hydraJobs.binaryTarball."x86_64-linux" - self.hydraJobs.binaryTarball."aarch64-darwin" - # Cross - self.hydraJobs.binaryTarballCross."x86_64-linux"."armv6l-unknown-linux-gnueabihf" - self.hydraJobs.binaryTarballCross."x86_64-linux"."armv7l-unknown-linux-gnueabihf" - self.hydraJobs.binaryTarballCross."x86_64-linux"."riscv64-unknown-linux-gnu" - ]; + + installerScriptForGHA = forAllSystems ( + system: + nixpkgsFor.${system}.native.callPackage ./installer { + tarballs = [ self.hydraJobs.binaryTarball.${system} ]; + } + ); # docker image with Nix inside dockerImage = lib.genAttrs linux64BitSystems (system: self.packages.${system}.dockerImage); @@ -147,25 +229,29 @@ in # }; # Nix's manual - manual = nixpkgsFor.x86_64-linux.native.nixComponents.nix-manual; + manual = nixpkgsFor.x86_64-linux.native.nixComponents2.nix-manual; # API docs for Nix's unstable internal C++ interfaces. - internal-api-docs = nixpkgsFor.x86_64-linux.native.nixComponents.nix-internal-api-docs; + internal-api-docs = nixpkgsFor.x86_64-linux.native.nixComponents2.nix-internal-api-docs; # API docs for Nix's C bindings. - external-api-docs = nixpkgsFor.x86_64-linux.native.nixComponents.nix-external-api-docs; + external-api-docs = nixpkgsFor.x86_64-linux.native.nixComponents2.nix-external-api-docs; # System tests. - tests = import ../tests/nixos { inherit lib nixpkgs nixpkgsFor self; } // { - - # Make sure that nix-env still produces the exact same result - # on a particular version of Nixpkgs. - evalNixpkgs = - let - inherit (nixpkgsFor.x86_64-linux.native) runCommand nix; - in - runCommand "eval-nixos" { buildInputs = [ nix ]; } - '' + tests = + import ../tests/nixos { + inherit lib nixpkgs nixpkgsFor; + inherit (self.inputs) nixpkgs-23-11; + } + // { + + # Make sure that nix-env still produces the exact same result + # on a particular version of Nixpkgs. + evalNixpkgs = + let + inherit (nixpkgsFor.x86_64-linux.native) runCommand nix; + in + runCommand "eval-nixos" { buildInputs = [ nix ]; } '' type -p nix-env # Note: we're filtering out nixos-install-tools because https://github.com/NixOS/nixpkgs/pull/153594#issuecomment-1020530593. ( @@ -176,36 +262,36 @@ in mkdir $out ''; - nixpkgsLibTests = - forAllSystems (system: - import (nixpkgs + "/lib/tests/test-with-nix.nix") - { - lib = nixpkgsFor.${system}.native.lib; - nix = self.packages.${system}.nix-cli; - pkgs = nixpkgsFor.${system}.native; - } + nixpkgsLibTests = forAllSystems ( + system: + import (nixpkgs + "/lib/tests/test-with-nix.nix") { + lib = nixpkgsFor.${system}.native.lib; + nix = self.packages.${system}.nix-cli; + pkgs = nixpkgsFor.${system}.native; + } ); - }; + }; metrics.nixpkgs = import "${nixpkgs-regression}/pkgs/top-level/metrics.nix" { pkgs = nixpkgsFor.x86_64-linux.native; nixpkgs = nixpkgs-regression; }; - installTests = forAllSystems (system: - let pkgs = nixpkgsFor.${system}.native; in - pkgs.runCommand "install-tests" - { - againstSelf = testNixVersions pkgs pkgs.nix; - againstCurrentLatest = - # FIXME: temporarily disable this on macOS because of #3605. - if system == "x86_64-linux" - then testNixVersions pkgs pkgs.nixVersions.latest - else null; - # Disabled because the latest stable version doesn't handle - # `NIX_DAEMON_SOCKET_PATH` which is required for the tests to work - # againstLatestStable = testNixVersions pkgs pkgs.nixStable; - } "touch $out"); + installTests = forAllSystems ( + system: + let + pkgs = nixpkgsFor.${system}.native; + in + pkgs.runCommand "install-tests" { + againstSelf = testNixVersions pkgs pkgs.nix; + againstCurrentLatest = + # FIXME: temporarily disable this on macOS because of #3605. + if system == "x86_64-linux" then testNixVersions pkgs pkgs.nixVersions.latest else null; + # Disabled because the latest stable version doesn't handle + # `NIX_DAEMON_SOCKET_PATH` which is required for the tests to work + # againstLatestStable = testNixVersions pkgs pkgs.nixStable; + } "touch $out" + ); installerTests = import ../tests/installer { binaryTarballs = self.hydraJobs.binaryTarball; diff --git a/packaging/installer/default.nix b/packaging/installer/default.nix new file mode 100644 index 00000000000..e171f36f99f --- /dev/null +++ b/packaging/installer/default.nix @@ -0,0 +1,42 @@ +{ + lib, + runCommand, + nix, + tarballs, +}: + +runCommand "installer-script" + { + buildInputs = [ nix ]; + } + '' + mkdir -p $out/nix-support + + # Converts /nix/store/50p3qk8k...-nix-2.4pre20201102_550e11f/bin/nix to 50p3qk8k.../bin/nix. + tarballPath() { + # Remove the store prefix + local path=''${1#${builtins.storeDir}/} + # Get the path relative to the derivation root + local rest=''${path#*/} + # Get the derivation hash + local drvHash=''${path%%-*} + echo "$drvHash/$rest" + } + + substitute ${./install.in} $out/install \ + ${ + lib.concatMapStrings ( + tarball: + let + inherit (tarball.stdenv.hostPlatform) system; + in + '' + \ + --replace '@tarballHash_${system}@' $(nix --experimental-features nix-command hash-file --base16 --type sha256 ${tarball}/*.tar.xz) \ + --replace '@tarballPath_${system}@' $(tarballPath ${tarball}/*.tar.xz) \ + '' + ) tarballs + } --replace '@nixVersion@' ${nix.version} + + echo "file installer $out/install" >> $out/nix-support/hydra-build-products + '' diff --git a/scripts/install.in b/packaging/installer/install.in similarity index 100% rename from scripts/install.in rename to packaging/installer/install.in diff --git a/precompiled-headers.h b/precompiled-headers.h deleted file mode 100644 index e1a3f8cc031..00000000000 --- a/precompiled-headers.h +++ /dev/null @@ -1,63 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef _WIN32 -# include -# include -# include -# include -# include -# include -# include -# include -# include -#endif - -#include diff --git a/scripts/build-checks b/scripts/build-checks new file mode 100755 index 00000000000..e0ee70631b5 --- /dev/null +++ b/scripts/build-checks @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail +system=$(nix eval --raw --impure --expr builtins.currentSystem) +nix eval --json ".#checks.$system" --apply builtins.attrNames | \ + jq -r '.[]' | \ + xargs -P0 -I '{}' sh -c "nix build -L .#checks.$system.{} || { echo 'FAILED: \033[0;31mnix build -L .#checks.$system.{}\\033[0m'; kill 0; }" diff --git a/scripts/create-darwin-volume.sh b/scripts/create-darwin-volume.sh index 103e1e3916c..7a61764d4f3 100755 --- a/scripts/create-darwin-volume.sh +++ b/scripts/create-darwin-volume.sh @@ -463,7 +463,7 @@ EOF EDITOR="$SCRATCH/ex_cleanroom_wrapper" _sudo "to add nix to fstab" "$@" < "$SCRATCH/dscl.err"; do + local err=$? + if [[ $err -eq 140 ]] && grep -q "-14988 (eNotYetImplemented)" "$SCRATCH/dscl.err"; then + echo "dscl failed with eNotYetImplemented, retrying..." + sleep 1 + continue + fi + cat "$SCRATCH/dscl.err" + return $err + done +} + poly_user_hidden_get() { dsclattr "/Users/$1" "IsHidden" } poly_user_hidden_set() { - _sudo "in order to make $1 a hidden user" \ - /usr/bin/dscl . -create "/Users/$1" "IsHidden" "1" + dscl_create "in order to make $1 a hidden user" \ + "/Users/$1" "IsHidden" "1" } poly_user_home_get() { @@ -161,8 +176,8 @@ poly_user_home_get() { poly_user_home_set() { # This can trigger a permission prompt now: # "Terminal" would like to administer your computer. Administration can include modifying passwords, networking, and system settings. - _sudo "in order to give $1 a safe home directory" \ - /usr/bin/dscl . -create "/Users/$1" "NFSHomeDirectory" "$2" + dscl_create "in order to give $1 a safe home directory" \ + "/Users/$1" "NFSHomeDirectory" "$2" } poly_user_note_get() { @@ -170,8 +185,8 @@ poly_user_note_get() { } poly_user_note_set() { - _sudo "in order to give $username a useful note" \ - /usr/bin/dscl . -create "/Users/$1" "RealName" "$2" + dscl_create "in order to give $1 a useful note" \ + "/Users/$1" "RealName" "$2" } poly_user_shell_get() { @@ -179,8 +194,8 @@ poly_user_shell_get() { } poly_user_shell_set() { - _sudo "in order to give $1 a safe shell" \ - /usr/bin/dscl . -create "/Users/$1" "UserShell" "$2" + dscl_create "in order to give $1 a safe shell" \ + "/Users/$1" "UserShell" "$2" } poly_user_in_group_check() { diff --git a/scripts/install-multi-user.sh b/scripts/install-multi-user.sh index a487d459f40..e9ddfc0140d 100644 --- a/scripts/install-multi-user.sh +++ b/scripts/install-multi-user.sh @@ -56,6 +56,9 @@ readonly NIX_INSTALLED_CACERT="@cacert@" #readonly NIX_INSTALLED_CACERT="/nix/store/7dxhzymvy330i28ii676fl1pqwcahv2f-nss-cacert-3.49.2" readonly EXTRACTED_NIX_PATH="$(dirname "$0")" +# allow to override identity change command +readonly NIX_BECOME=${NIX_BECOME:-sudo} + readonly ROOT_HOME=~root if [ -t 0 ] && [ -z "${NIX_INSTALLER_YES:-}" ]; then @@ -123,7 +126,7 @@ uninstall_directions() { cat < "$SCRATCH/.nix-channels" _sudo "to set up the default system channel (part 1)" \ - install -m 0664 "$SCRATCH/.nix-channels" "$ROOT_HOME/.nix-channels" + install -m 0644 "$SCRATCH/.nix-channels" "$ROOT_HOME/.nix-channels" fi } @@ -829,8 +834,13 @@ install_from_extracted_nix() { ( cd "$EXTRACTED_NIX_PATH" - _sudo "to copy the basic Nix files to the new store at $NIX_ROOT/store" \ - cp -RPp ./store/* "$NIX_ROOT/store/" + if is_os_darwin; then + _sudo "to copy the basic Nix files to the new store at $NIX_ROOT/store" \ + cp -RPp ./store/* "$NIX_ROOT/store/" + else + _sudo "to copy the basic Nix files to the new store at $NIX_ROOT/store" \ + cp -RP --preserve=ownership,timestamps ./store/* "$NIX_ROOT/store/" + fi _sudo "to make the new store non-writable at $NIX_ROOT/store" \ chmod -R ugo-w "$NIX_ROOT/store/" @@ -964,7 +974,7 @@ $NIX_EXTRA_CONF build-users-group = $NIX_BUILD_GROUP_NAME EOF _sudo "to place the default nix daemon configuration (part 2)" \ - install -m 0664 "$SCRATCH/nix.conf" /etc/nix/nix.conf + install -m 0644 "$SCRATCH/nix.conf" /etc/nix/nix.conf } diff --git a/scripts/install-nix-from-tarball.sh b/scripts/install-nix-from-tarball.sh index 007fe85ee94..ec326479323 100644 --- a/scripts/install-nix-from-tarball.sh +++ b/scripts/install-nix-from-tarball.sh @@ -9,6 +9,8 @@ self="$(dirname "$0")" nix="@nix@" cacert="@cacert@" +# allow to override identity change command +readonly NIX_BECOME="${NIX_BECOME:-sudo}" if ! [ -e "$self/.reginfo" ]; then echo "$0: incomplete installer (.reginfo is missing)" >&2 @@ -63,7 +65,6 @@ while [ $# -gt 0 ]; do exit 1 fi INSTALL_MODE=no-daemon - # intentional tail space ACTION=install ;; --yes) @@ -135,8 +136,8 @@ echo "performing a single-user installation of Nix..." >&2 if ! [ -e "$dest" ]; then cmd="mkdir -m 0755 $dest && chown $USER $dest" - echo "directory $dest does not exist; creating it by running '$cmd' using sudo" >&2 - if ! sudo sh -c "$cmd"; then + echo "directory $dest does not exist; creating it by running '$cmd' using $NIX_BECOME" >&2 + if ! $NIX_BECOME sh -c "$cmd"; then echo "$0: please manually run '$cmd' as root to create $dest" >&2 exit 1 fi @@ -166,7 +167,11 @@ for i in $(cd "$self/store" >/dev/null && echo ./*); do rm -rf "$i_tmp" fi if ! [ -e "$dest/store/$i" ]; then - cp -RPp "$self/store/$i" "$i_tmp" + if [ "$(uname -s)" = "Darwin" ]; then + cp -RPp "$self/store/$i" "$i_tmp" + else + cp -RP --preserve=ownership,timestamps "$self/store/$i" "$i_tmp" + fi chmod -R a-w "$i_tmp" chmod +w "$i_tmp" mv "$i_tmp" "$dest/store/$i" diff --git a/scripts/installer.nix b/scripts/installer.nix deleted file mode 100644 index cc7759c2c8e..00000000000 --- a/scripts/installer.nix +++ /dev/null @@ -1,36 +0,0 @@ -{ lib -, runCommand -, nix -, tarballs -}: - -runCommand "installer-script" { - buildInputs = [ nix ]; -} '' - mkdir -p $out/nix-support - - # Converts /nix/store/50p3qk8k...-nix-2.4pre20201102_550e11f/bin/nix to 50p3qk8k.../bin/nix. - tarballPath() { - # Remove the store prefix - local path=''${1#${builtins.storeDir}/} - # Get the path relative to the derivation root - local rest=''${path#*/} - # Get the derivation hash - local drvHash=''${path%%-*} - echo "$drvHash/$rest" - } - - substitute ${./install.in} $out/install \ - ${lib.concatMapStrings - (tarball: let - inherit (tarball.stdenv.hostPlatform) system; - in '' \ - --replace '@tarballHash_${system}@' $(nix --experimental-features nix-command hash-file --base16 --type sha256 ${tarball}/*.tar.xz) \ - --replace '@tarballPath_${system}@' $(tarballPath ${tarball}/*.tar.xz) \ - '' - ) - tarballs - } --replace '@nixVersion@' ${nix.version} - - echo "file installer $out/install" >> $out/nix-support/hydra-build-products -'' diff --git a/scripts/nix-profile-daemon.fish.in b/scripts/nix-profile-daemon.fish.in index 346dce5ddcf..1a20dffd245 100644 --- a/scripts/nix-profile-daemon.fish.in +++ b/scripts/nix-profile-daemon.fish.in @@ -1,7 +1,16 @@ +# Only execute this file once per shell. +if test -z "$HOME" || test -n "$__ETC_PROFILE_NIX_SOURCED" + exit +end + +set --global --export __ETC_PROFILE_NIX_SOURCED 1 + +# Local helpers + function add_path --argument-names new_path if type -q fish_add_path # fish 3.2.0 or newer - fish_add_path --prepend --global $new_path + fish_add_path --prepend --global $new_path else # older versions of fish if not contains $new_path $fish_user_paths @@ -10,48 +19,84 @@ function add_path --argument-names new_path end end -# Only execute this file once per shell. -if test -n "$__ETC_PROFILE_NIX_SOURCED" - exit +# Main configuration + +# Set up the per-user profile. + +set --local NIX_LINK "$HOME/.nix-profile" +set --local NIX_LINK_NEW +if test -n "$XDG_STATE_HOME" + set NIX_LINK_NEW "$XDG_STATE_HOME/nix/profile" +else + set NIX_LINK_NEW "$HOME/.local/state/nix/profile" end +if test -e "$NIX_LINK_NEW" + if test -t 2; and test -e "$NIX_LINK" + set --local warning "\033[1;35mwarning:\033[0m " + printf "$warning Both %s and legacy %s exist; using the former.\n" "$NIX_LINK_NEW" "$NIX_LINK" 1>&2 -set __ETC_PROFILE_NIX_SOURCED 1 + if test (realpath "$NIX_LINK") = (realpath "$NIX_LINK_NEW") + printf " Since the profiles match, you can safely delete either of them.\n" 1>&2 + else + # This should be an exceptionally rare occasion: the only way to get it would be to + # 1. Update to newer Nix; + # 2. Remove .nix-profile; + # 3. Set the $NIX_LINK_NEW to something other than the default user profile; + # 4. Roll back to older Nix. + # If someone did all that, they can probably figure out how to migrate the profile. + printf "$warning Profiles do not match. You should manually migrate from %s to %s.\n" "$NIX_LINK" "$NIX_LINK_NEW" 1>&2 + end + end + + set NIX_LINK "$NIX_LINK_NEW" +end +# Set up environment. +# This part should be kept in sync with nixpkgs:nixos/modules/programs/environment.nix set --export NIX_PROFILES "@localstatedir@/nix/profiles/default $HOME/.nix-profile" # Populate bash completions, .desktop files, etc if test -z "$XDG_DATA_DIRS" # According to XDG spec the default is /usr/local/share:/usr/share, don't set something that prevents that default - set --export XDG_DATA_DIRS "/usr/local/share:/usr/share:/nix/var/nix/profiles/default/share" + set --export XDG_DATA_DIRS "/usr/local/share:/usr/share:$NIX_LINK/share:/nix/var/nix/profiles/default/share" else - set --export XDG_DATA_DIRS "$XDG_DATA_DIRS:/nix/var/nix/profiles/default/share" + set --export XDG_DATA_DIRS "$XDG_DATA_DIRS:$NIX_LINK/share:/nix/var/nix/profiles/default/share" end # Set $NIX_SSL_CERT_FILE so that Nixpkgs applications like curl work. if test -n "$NIX_SSL_CERT_FILE" - : # Allow users to override the NIX_SSL_CERT_FILE + : # Allow users to override the NIX_SSL_CERT_FILE else if test -e /etc/ssl/certs/ca-certificates.crt # NixOS, Ubuntu, Debian, Gentoo, Arch - set --export NIX_SSL_CERT_FILE /etc/ssl/certs/ca-certificates.crt + set --export NIX_SSL_CERT_FILE /etc/ssl/certs/ca-certificates.crt else if test -e /etc/ssl/ca-bundle.pem # openSUSE Tumbleweed - set --export NIX_SSL_CERT_FILE /etc/ssl/ca-bundle.pem + set --export NIX_SSL_CERT_FILE /etc/ssl/ca-bundle.pem else if test -e /etc/ssl/certs/ca-bundle.crt # Old NixOS - set --export NIX_SSL_CERT_FILE /etc/ssl/certs/ca-bundle.crt + set --export NIX_SSL_CERT_FILE /etc/ssl/certs/ca-bundle.crt else if test -e /etc/pki/tls/certs/ca-bundle.crt # Fedora, CentOS - set --export NIX_SSL_CERT_FILE /etc/pki/tls/certs/ca-bundle.crt + set --export NIX_SSL_CERT_FILE /etc/pki/tls/certs/ca-bundle.crt else if test -e "$NIX_LINK/etc/ssl/certs/ca-bundle.crt" # fall back to cacert in Nix profile - set --export NIX_SSL_CERT_FILE "$NIX_LINK/etc/ssl/certs/ca-bundle.crt" + set --export NIX_SSL_CERT_FILE "$NIX_LINK/etc/ssl/certs/ca-bundle.crt" else if test -e "$NIX_LINK/etc/ca-bundle.crt" # old cacert in Nix profile - set --export NIX_SSL_CERT_FILE "$NIX_LINK/etc/ca-bundle.crt" + set --export NIX_SSL_CERT_FILE "$NIX_LINK/etc/ca-bundle.crt" else - # Fall back to what is in the nix profiles, favouring whatever is defined last. - for i in (string split ' ' $NIX_PROFILES) - if test -e "$i/etc/ssl/certs/ca-bundle.crt" - set --export NIX_SSL_CERT_FILE "$i/etc/ssl/certs/ca-bundle.crt" + # Fall back to what is in the nix profiles, favouring whatever is defined last. + for i in (string split ' ' $NIX_PROFILES) + if test -e "$i/etc/ssl/certs/ca-bundle.crt" + set --export NIX_SSL_CERT_FILE "$i/etc/ssl/certs/ca-bundle.crt" + end end - end +end + +# Only use MANPATH if it is already set. In general `man` will just simply +# pick up `.nix-profile/share/man` because is it close to `.nix-profile/bin` +# which is in the $PATH. For more info, run `manpath -d`. +if set --query MANPATH + set --export --prepend --path MANPATH "$NIX_LINK/share/man" end add_path "@localstatedir@/nix/profiles/default/bin" -add_path "$HOME/.nix-profile/bin" +add_path "$NIX_LINK/bin" + +# Cleanup functions -e add_path diff --git a/scripts/nix-profile-daemon.sh.in b/scripts/nix-profile-daemon.sh.in index 59c00d49191..ed74c242a82 100644 --- a/scripts/nix-profile-daemon.sh.in +++ b/scripts/nix-profile-daemon.sh.in @@ -1,7 +1,7 @@ # Only execute this file once per shell. # This file is tested by tests/installer/default.nix. if [ -n "${__ETC_PROFILE_NIX_SOURCED:-}" ]; then return; fi -__ETC_PROFILE_NIX_SOURCED=1 +export __ETC_PROFILE_NIX_SOURCED=1 NIX_LINK=$HOME/.nix-profile if [ -n "${XDG_STATE_HOME-}" ]; then diff --git a/scripts/nix-profile.fish.in b/scripts/nix-profile.fish.in index 619df52b895..abf716cec6f 100644 --- a/scripts/nix-profile.fish.in +++ b/scripts/nix-profile.fish.in @@ -1,7 +1,16 @@ +# Only execute this file once per shell. +if test -z "$HOME" || test -n "$__ETC_PROFILE_NIX_SOURCED" + exit +end + +set --global --export __ETC_PROFILE_NIX_SOURCED 1 + +# Local helpers + function add_path --argument-names new_path if type -q fish_add_path # fish 3.2.0 or newer - fish_add_path --prepend --global $new_path + fish_add_path --prepend --global $new_path else # older versions of fish if not contains $new_path $fish_user_paths @@ -10,50 +19,89 @@ function add_path --argument-names new_path end end -if test -n "$HOME" && test -n "$USER" +# Main configuration - # Set up the per-user profile. +# Set up the per-user profile. - set NIX_LINK $HOME/.nix-profile - - # Set up environment. - # This part should be kept in sync with nixpkgs:nixos/modules/programs/environment.nix - set --export NIX_PROFILES "@localstatedir@/nix/profiles/default $HOME/.nix-profile" - - # Populate bash completions, .desktop files, etc - if test -z "$XDG_DATA_DIRS" - # According to XDG spec the default is /usr/local/share:/usr/share, don't set something that prevents that default - set --export XDG_DATA_DIRS "/usr/local/share:/usr/share:$NIX_LINK/share:/nix/var/nix/profiles/default/share" +set --local NIX_LINK +if test -n "$NIX_STATE_HOME" + set NIX_LINK "$NIX_STATE_HOME/.nix-profile" +else + set NIX_LINK "$HOME/.nix-profile" + set --local NIX_LINK_NEW + if test -n "$XDG_STATE_HOME" + set NIX_LINK_NEW "$XDG_STATE_HOME/nix/profile" else - set --export XDG_DATA_DIRS "$XDG_DATA_DIRS:$NIX_LINK/share:/nix/var/nix/profiles/default/share" + set NIX_LINK_NEW "$HOME/.local/state/nix/profile" end + if test -e "$NIX_LINK_NEW" + if test -t 2; and test -e "$NIX_LINK" + set --local warning "\033[1;35mwarning:\033[0m " + printf "$warning Both %s and legacy %s exist; using the former.\n" "$NIX_LINK_NEW" "$NIX_LINK" 1>&2 + + if test (realpath "$NIX_LINK") = (realpath "$NIX_LINK_NEW") + printf " Since the profiles match, you can safely delete either of them.\n" 1>&2 + else + # This should be an exceptionally rare occasion: the only way to get it would be to + # 1. Update to newer Nix; + # 2. Remove .nix-profile; + # 3. Set the $NIX_LINK_NEW to something other than the default user profile; + # 4. Roll back to older Nix. + # If someone did all that, they can probably figure out how to migrate the profile. + printf "$warning Profiles do not match. You should manually migrate from %s to %s.\n" "$NIX_LINK" "$NIX_LINK_NEW" 1>&2 + end + end - # Set $NIX_SSL_CERT_FILE so that Nixpkgs applications like curl work. - if test -n "$NIX_SSH_CERT_FILE" - : # Allow users to override the NIX_SSL_CERT_FILE - else if test -e /etc/ssl/certs/ca-certificates.crt # NixOS, Ubuntu, Debian, Gentoo, Arch - set --export NIX_SSL_CERT_FILE /etc/ssl/certs/ca-certificates.crt - else if test -e /etc/ssl/ca-bundle.pem # openSUSE Tumbleweed - set --export NIX_SSL_CERT_FILE /etc/ssl/ca-bundle.pem - else if test -e /etc/ssl/certs/ca-bundle.crt # Old NixOS - set --export NIX_SSL_CERT_FILE /etc/ssl/certs/ca-bundle.crt - else if test -e /etc/pki/tls/certs/ca-bundle.crt # Fedora, CentOS - set --export NIX_SSL_CERT_FILE /etc/pki/tls/certs/ca-bundle.crt - else if test -e "$NIX_LINK/etc/ssl/certs/ca-bundle.crt" # fall back to cacert in Nix profile - set --export NIX_SSL_CERT_FILE "$NIX_LINK/etc/ssl/certs/ca-bundle.crt" - else if test -e "$NIX_LINK/etc/ca-bundle.crt" # old cacert in Nix profile - set --export NIX_SSL_CERT_FILE "$NIX_LINK/etc/ca-bundle.crt" + set NIX_LINK "$NIX_LINK_NEW" end +end + +# Set up environment. +# This part should be kept in sync with nixpkgs:nixos/modules/programs/environment.nix +set --export NIX_PROFILES "@localstatedir@/nix/profiles/default $HOME/.nix-profile" + +# Populate bash completions, .desktop files, etc +if test -z "$XDG_DATA_DIRS" + # According to XDG spec the default is /usr/local/share:/usr/share, don't set something that prevents that default + set --export XDG_DATA_DIRS "/usr/local/share:/usr/share:$NIX_LINK/share:/nix/var/nix/profiles/default/share" +else + set --export XDG_DATA_DIRS "$XDG_DATA_DIRS:$NIX_LINK/share:/nix/var/nix/profiles/default/share" +end - # Only use MANPATH if it is already set. In general `man` will just simply - # pick up `.nix-profile/share/man` because is it close to `.nix-profile/bin` - # which is in the $PATH. For more info, run `manpath -d`. - if set --query MANPATH - set --export --prepend --path MANPATH "$NIX_LINK/share/man" +# Set $NIX_SSL_CERT_FILE so that Nixpkgs applications like curl work. +if test -n "$NIX_SSL_CERT_FILE" + : # Allow users to override the NIX_SSL_CERT_FILE +else if test -e /etc/ssl/certs/ca-certificates.crt # NixOS, Ubuntu, Debian, Gentoo, Arch + set --export NIX_SSL_CERT_FILE /etc/ssl/certs/ca-certificates.crt +else if test -e /etc/ssl/ca-bundle.pem # openSUSE Tumbleweed + set --export NIX_SSL_CERT_FILE /etc/ssl/ca-bundle.pem +else if test -e /etc/ssl/certs/ca-bundle.crt # Old NixOS + set --export NIX_SSL_CERT_FILE /etc/ssl/certs/ca-bundle.crt +else if test -e /etc/pki/tls/certs/ca-bundle.crt # Fedora, CentOS + set --export NIX_SSL_CERT_FILE /etc/pki/tls/certs/ca-bundle.crt +else if test -e "$NIX_LINK/etc/ssl/certs/ca-bundle.crt" # fall back to cacert in Nix profile + set --export NIX_SSL_CERT_FILE "$NIX_LINK/etc/ssl/certs/ca-bundle.crt" +else if test -e "$NIX_LINK/etc/ca-bundle.crt" # old cacert in Nix profile + set --export NIX_SSL_CERT_FILE "$NIX_LINK/etc/ca-bundle.crt" +else + # Fall back to what is in the nix profiles, favouring whatever is defined last. + for i in (string split ' ' $NIX_PROFILES) + if test -e "$i/etc/ssl/certs/ca-bundle.crt" + set --export NIX_SSL_CERT_FILE "$i/etc/ssl/certs/ca-bundle.crt" + end end +end - add_path "$NIX_LINK/bin" - set --erase NIX_LINK +# Only use MANPATH if it is already set. In general `man` will just simply +# pick up `.nix-profile/share/man` because is it close to `.nix-profile/bin` +# which is in the $PATH. For more info, run `manpath -d`. +if set --query MANPATH + set --export --prepend --path MANPATH "$NIX_LINK/share/man" end +add_path "@localstatedir@/nix/profiles/default/bin" +add_path "$NIX_LINK/bin" + +# Cleanup + functions -e add_path diff --git a/scripts/prepare-installer-for-github-actions b/scripts/prepare-installer-for-github-actions index 4b994a7532c..0fbecf25c2a 100755 --- a/scripts/prepare-installer-for-github-actions +++ b/scripts/prepare-installer-for-github-actions @@ -1,10 +1,11 @@ #!/usr/bin/env bash -set -e +set -euo pipefail -script=$(nix-build -A outputs.hydraJobs.installerScriptForGHA --no-out-link) -installerHash=$(echo "$script" | cut -b12-43 -) +nix build -L ".#installerScriptForGHA" ".#binaryTarball" -installerURL=https://$CACHIX_NAME.cachix.org/serve/$installerHash/install - -echo "::set-output name=installerURL::$installerURL" +mkdir -p out +cp ./result/install "out/install" +name="$(basename "$(realpath ./result-1)")" +# everything before the first dash +cp -r ./result-1 "out/${name%%-*}" diff --git a/scripts/sequoia-nixbld-user-migration.sh b/scripts/sequoia-nixbld-user-migration.sh index 88e8017065a..58b5fea64db 100755 --- a/scripts/sequoia-nixbld-user-migration.sh +++ b/scripts/sequoia-nixbld-user-migration.sh @@ -2,6 +2,9 @@ set -eo pipefail +# stock path to avoid unexpected command versions +PATH="$(/usr/bin/getconf PATH)" + ((NEW_NIX_FIRST_BUILD_UID=351)) ((TEMP_NIX_FIRST_BUILD_UID=31000)) diff --git a/scripts/serve-installer-for-github-actions b/scripts/serve-installer-for-github-actions new file mode 100755 index 00000000000..2efd2aa329e --- /dev/null +++ b/scripts/serve-installer-for-github-actions @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -euo pipefail +if [[ ! -d out ]]; then + echo "run prepare-installer-for-github-actions first" + exit 1 +fi +cd out +PORT=${PORT:-8126} +nohup python -m http.server "$PORT" >/dev/null 2>&1 & +pid=$! + +while ! curl -s "http://localhost:$PORT"; do + sleep 1 + if ! kill -0 $pid; then + echo "Failed to start http server" + exit 1 + fi +done + +echo 'To install nix, run the following command:' +echo "sh <(curl http://localhost:$PORT/install) --tarball-url-prefix http://localhost:$PORT" diff --git a/src/build-remote/build-remote.cc b/src/build-remote/build-remote.cc index 82ad7d86212..cd13e66706d 100644 --- a/src/build-remote/build-remote.cc +++ b/src/build-remote/build-remote.cc @@ -5,23 +5,23 @@ #include #include #include -#if __APPLE__ +#ifdef __APPLE__ #include #endif -#include "machines.hh" -#include "shared.hh" -#include "plugin.hh" -#include "pathlocks.hh" -#include "globals.hh" -#include "serialise.hh" -#include "build-result.hh" -#include "store-api.hh" -#include "strings.hh" -#include "derivations.hh" -#include "local-store.hh" -#include "legacy.hh" -#include "experimental-features.hh" +#include "nix/store/machines.hh" +#include "nix/main/shared.hh" +#include "nix/main/plugin.hh" +#include "nix/store/pathlocks.hh" +#include "nix/store/globals.hh" +#include "nix/util/serialise.hh" +#include "nix/store/build-result.hh" +#include "nix/store/store-open.hh" +#include "nix/util/strings.hh" +#include "nix/store/derivations.hh" +#include "nix/store/local-store.hh" +#include "nix/cmd/legacy.hh" +#include "nix/util/experimental-features.hh" using namespace nix; using std::cin; @@ -42,16 +42,16 @@ static AutoCloseFD openSlotLock(const Machine & m, uint64_t slot) return openLockFile(fmt("%s/%s-%d", currentLoad, escapeUri(m.storeUri.render()), slot), true); } -static bool allSupportedLocally(Store & store, const std::set& requiredFeatures) { +static bool allSupportedLocally(Store & store, const StringSet& requiredFeatures) { for (auto & feature : requiredFeatures) - if (!store.systemFeatures.get().count(feature)) return false; + if (!store.config.systemFeatures.get().count(feature)) return false; return true; } static int main_build_remote(int argc, char * * argv) { { - logger = makeJSONLogger(*logger); + logger = makeJSONLogger(getStandardError()); /* Ensure we don't get any SSH passphrase or host key popups. */ unsetenv("DISPLAY"); @@ -85,7 +85,7 @@ static int main_build_remote(int argc, char * * argv) that gets cleared on reboot, but it wouldn't work on macOS. */ auto currentLoadName = "/current-load"; if (auto localStore = store.dynamic_pointer_cast()) - currentLoad = std::string { localStore->stateDir } + currentLoadName; + currentLoad = std::string { localStore->config.stateDir } + currentLoadName; else currentLoad = settings.nixStateDir + currentLoadName; @@ -113,7 +113,7 @@ static int main_build_remote(int argc, char * * argv) auto amWilling = readInt(source); auto neededSystem = readString(source); drvPath = store->parseStorePath(readString(source)); - auto requiredFeatures = readStrings>(source); + auto requiredFeatures = readStrings(source); /* It would be possible to build locally after some builds clear out, so don't show the warning now: */ @@ -225,7 +225,7 @@ static int main_build_remote(int argc, char * * argv) break; } -#if __APPLE__ +#ifdef __APPLE__ futimes(bestSlotLock.get(), NULL); #else futimens(bestSlotLock.get(), NULL); @@ -329,8 +329,17 @@ static int main_build_remote(int argc, char * * argv) drv.inputSrcs = store->parseStorePathSet(inputs); optResult = sshStore->buildDerivation(*drvPath, (const BasicDerivation &) drv); auto & result = *optResult; - if (!result.success()) + if (!result.success()) { + if (settings.keepFailed) { + warn( + "The failed build directory was kept on the remote builder due to `--keep-failed`.%s", + (settings.thisSystem == drv.platform || settings.extraPlatforms.get().count(drv.platform) > 0) + ? " You can re-run the command with `--builders ''` to disable remote building for this invocation." + : "" + ); + } throw Error("build of '%s' on '%s' failed: %s", store->printStorePath(*drvPath), storeUri, result.errorMsg); + } } else { copyClosure(*store, *sshStore, StorePathSet {*drvPath}, NoRepair, NoCheckSigs, substitute); auto res = sshStore->buildPathsWithResults({ diff --git a/src/external-api-docs/package.nix b/src/external-api-docs/package.nix index 57c5138cfdb..b194e16d460 100644 --- a/src/external-api-docs/package.nix +++ b/src/external-api-docs/package.nix @@ -1,11 +1,12 @@ -{ lib -, mkMesonDerivation +{ + lib, + mkMesonDerivation, -, doxygen + doxygen, -# Configuration Options + # Configuration Options -, version + version, }: let @@ -39,11 +40,10 @@ mkMesonDerivation (finalAttrs: { doxygen ]; - preConfigure = - '' - chmod u+w ./.version - echo ${finalAttrs.version} > ./.version - ''; + preConfigure = '' + chmod u+w ./.version + echo ${finalAttrs.version} > ./.version + ''; postInstall = '' mkdir -p ''${!outputDoc}/nix-support diff --git a/src/internal-api-docs/package.nix b/src/internal-api-docs/package.nix index 993a257a69f..6c4f354aee5 100644 --- a/src/internal-api-docs/package.nix +++ b/src/internal-api-docs/package.nix @@ -1,11 +1,12 @@ -{ lib -, mkMesonDerivation +{ + lib, + mkMesonDerivation, -, doxygen + doxygen, -# Configuration Options + # Configuration Options -, version + version, }: let @@ -17,27 +18,28 @@ mkMesonDerivation (finalAttrs: { inherit version; workDir = ./.; - fileset = let - cpp = fileset.fileFilter (file: file.hasExt "cc" || file.hasExt "hh"); - in fileset.unions [ - ./.version - ../../.version - ./meson.build - ./doxygen.cfg.in - # Source is not compiled, but still must be available for Doxygen - # to gather comments. - (cpp ../.) - ]; + fileset = + let + cpp = fileset.fileFilter (file: file.hasExt "cc" || file.hasExt "hh"); + in + fileset.unions [ + ./.version + ../../.version + ./meson.build + ./doxygen.cfg.in + # Source is not compiled, but still must be available for Doxygen + # to gather comments. + (cpp ../.) + ]; nativeBuildInputs = [ doxygen ]; - preConfigure = - '' - chmod u+w ./.version - echo ${finalAttrs.version} > ./.version - ''; + preConfigure = '' + chmod u+w ./.version + echo ${finalAttrs.version} > ./.version + ''; postInstall = '' mkdir -p ''${!outputDoc}/nix-support diff --git a/src/libcmd/build-utils-meson b/src/libcmd/build-utils-meson deleted file mode 120000 index 5fff21bab55..00000000000 --- a/src/libcmd/build-utils-meson +++ /dev/null @@ -1 +0,0 @@ -../../build-utils-meson \ No newline at end of file diff --git a/src/libcmd/built-path.cc b/src/libcmd/built-path.cc index 905e70f32c9..1238f942254 100644 --- a/src/libcmd/built-path.cc +++ b/src/libcmd/built-path.cc @@ -1,7 +1,7 @@ -#include "built-path.hh" -#include "derivations.hh" -#include "store-api.hh" -#include "comparator.hh" +#include "nix/cmd/built-path.hh" +#include "nix/store/derivations.hh" +#include "nix/store/store-api.hh" +#include "nix/util/comparator.hh" #include diff --git a/src/libcmd/command-installable-value.cc b/src/libcmd/command-installable-value.cc index 7e0c15eb8cb..0884f17e927 100644 --- a/src/libcmd/command-installable-value.cc +++ b/src/libcmd/command-installable-value.cc @@ -1,4 +1,4 @@ -#include "command-installable-value.hh" +#include "nix/cmd/command-installable-value.hh" namespace nix { diff --git a/src/libcmd/command.cc b/src/libcmd/command.cc index 86d13fab796..31f64fd5a8d 100644 --- a/src/libcmd/command.cc +++ b/src/libcmd/command.cc @@ -1,25 +1,23 @@ #include #include -#include "command.hh" -#include "markdown.hh" -#include "store-api.hh" -#include "local-fs-store.hh" -#include "derivations.hh" -#include "nixexpr.hh" -#include "profiles.hh" -#include "repl.hh" -#include "strings.hh" -#include "environment-variables.hh" +#include "nix/cmd/command.hh" +#include "nix/cmd/markdown.hh" +#include "nix/store/store-open.hh" +#include "nix/store/local-fs-store.hh" +#include "nix/store/derivations.hh" +#include "nix/expr/nixexpr.hh" +#include "nix/store/profiles.hh" +#include "nix/cmd/repl.hh" +#include "nix/util/strings.hh" +#include "nix/util/environment-variables.hh" namespace nix { -RegisterCommand::Commands * RegisterCommand::commands = nullptr; - nix::Commands RegisterCommand::getCommandsFor(const std::vector & prefix) { nix::Commands res; - for (auto & [name, command] : *RegisterCommand::commands) + for (auto & [name, command] : RegisterCommand::commands()) if (name.size() == prefix.size() + 1) { bool equal = true; for (size_t i = 0; i < prefix.size(); ++i) @@ -40,7 +38,7 @@ nlohmann::json NixMultiCommand::toJSON() void NixMultiCommand::run() { if (!command) { - std::set subCommandTextLines; + StringSet subCommandTextLines; for (auto & [name, _] : commands) subCommandTextLines.insert(fmt("- `%s`", name)); std::string markdownError = @@ -237,12 +235,13 @@ void StorePathCommand::run(ref store, StorePaths && storePaths) MixProfile::MixProfile() { - addFlag( - {.longName = "profile", - .description = "The profile to operate on.", - .labels = {"path"}, - .handler = {&profile}, - .completer = completePath}); + addFlag({ + .longName = "profile", + .description = "The profile to operate on.", + .labels = {"path"}, + .handler = {&profile}, + .completer = completePath, + }); } void MixProfile::updateProfile(const StorePath & storePath) @@ -396,4 +395,11 @@ void createOutLinks(const std::filesystem::path & outLink, const BuiltPaths & bu } } +void MixOutLinkBase::createOutLinksMaybe(const std::vector & buildables, ref & store) +{ + if (outLink != "") + if (auto store2 = store.dynamic_pointer_cast()) + createOutLinks(outLink, toBuiltPaths(buildables), *store2); +} + } diff --git a/src/libcmd/common-eval-args.cc b/src/libcmd/common-eval-args.cc index de967e3fe49..d275beb12c3 100644 --- a/src/libcmd/common-eval-args.cc +++ b/src/libcmd/common-eval-args.cc @@ -1,24 +1,23 @@ -#include "fetch-settings.hh" -#include "eval-settings.hh" -#include "common-eval-args.hh" -#include "shared.hh" -#include "config-global.hh" -#include "filetransfer.hh" -#include "eval.hh" -#include "fetchers.hh" -#include "registry.hh" -#include "flake/flakeref.hh" -#include "flake/settings.hh" -#include "store-api.hh" -#include "command.hh" -#include "tarball.hh" -#include "fetch-to-store.hh" -#include "compatibility-settings.hh" -#include "eval-settings.hh" +#include "nix/fetchers/fetch-settings.hh" +#include "nix/expr/eval-settings.hh" +#include "nix/cmd/common-eval-args.hh" +#include "nix/main/shared.hh" +#include "nix/util/config-global.hh" +#include "nix/store/filetransfer.hh" +#include "nix/expr/eval.hh" +#include "nix/fetchers/fetchers.hh" +#include "nix/fetchers/registry.hh" +#include "nix/flake/flakeref.hh" +#include "nix/flake/settings.hh" +#include "nix/store/store-open.hh" +#include "nix/cmd/command.hh" +#include "nix/fetchers/tarball.hh" +#include "nix/fetchers/fetch-to-store.hh" +#include "nix/cmd/compatibility-settings.hh" +#include "nix/expr/eval-settings.hh" namespace nix { -namespace fs { using namespace std::filesystem; } fetchers::Settings fetchSettings; @@ -34,8 +33,15 @@ EvalSettings evalSettings { // FIXME `parseFlakeRef` should take a `std::string_view`. auto flakeRef = parseFlakeRef(fetchSettings, std::string { rest }, {}, true, false); debug("fetching flake search path element '%s''", rest); - auto storePath = flakeRef.resolve(state.store).fetchTree(state.store).first; - return state.rootPath(state.store->toRealPath(storePath)); + auto [accessor, lockedRef] = flakeRef.resolve(state.store).lazyFetch(state.store); + auto storePath = nix::fetchToStore( + state.fetchSettings, + *state.store, + SourcePath(accessor), + FetchMode::Copy, + lockedRef.input.getName()); + state.allowPath(storePath); + return state.storePath(storePath); }, }, }, @@ -61,7 +67,7 @@ MixEvalArgs::MixEvalArgs() .description = "Pass the value *expr* as the argument *name* to Nix functions.", .category = category, .labels = {"name", "expr"}, - .handler = {[&](std::string name, std::string expr) { autoArgs.insert_or_assign(name, AutoArg{AutoArgExpr{expr}}); }} + .handler = {[&](std::string name, std::string expr) { autoArgs.insert_or_assign(name, AutoArg{AutoArgExpr{expr}}); }}, }); addFlag({ @@ -78,7 +84,7 @@ MixEvalArgs::MixEvalArgs() .category = category, .labels = {"name", "path"}, .handler = {[&](std::string name, std::string path) { autoArgs.insert_or_assign(name, AutoArg{AutoArgFile{path}}); }}, - .completer = completePath + .completer = completePath, }); addFlag({ @@ -103,7 +109,7 @@ MixEvalArgs::MixEvalArgs() .labels = {"path"}, .handler = {[&](std::string s) { lookupPath.elements.emplace_back(LookupPath::Elem::parse(s)); - }} + }}, }); addFlag({ @@ -121,15 +127,15 @@ MixEvalArgs::MixEvalArgs() .category = category, .labels = {"original-ref", "resolved-ref"}, .handler = {[&](std::string _from, std::string _to) { - auto from = parseFlakeRef(fetchSettings, _from, fs::current_path().string()); - auto to = parseFlakeRef(fetchSettings, _to, fs::current_path().string()); + auto from = parseFlakeRef(fetchSettings, _from, std::filesystem::current_path().string()); + auto to = parseFlakeRef(fetchSettings, _to, std::filesystem::current_path().string()); fetchers::Attrs extraAttrs; if (to.subdir != "") extraAttrs["dir"] = to.subdir; fetchers::overrideRegistry(from.input, to.input, extraAttrs); }}, .completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) { completeFlakeRef(completions, openStore(), prefix); - }} + }}, }); addFlag({ @@ -176,15 +182,26 @@ SourcePath lookupFileArg(EvalState & state, std::string_view s, const Path * bas state.store, state.fetchSettings, EvalSettings::resolvePseudoUrl(s)); - auto storePath = fetchToStore(*state.store, SourcePath(accessor), FetchMode::Copy); - return state.rootPath(CanonPath(state.store->toRealPath(storePath))); + auto storePath = fetchToStore( + state.fetchSettings, + *state.store, + SourcePath(accessor), + FetchMode::Copy); + return state.storePath(storePath); } else if (hasPrefix(s, "flake:")) { experimentalFeatureSettings.require(Xp::Flakes); auto flakeRef = parseFlakeRef(fetchSettings, std::string(s.substr(6)), {}, true, false); - auto storePath = flakeRef.resolve(state.store).fetchTree(state.store).first; - return state.rootPath(CanonPath(state.store->toRealPath(storePath))); + auto [accessor, lockedRef] = flakeRef.resolve(state.store).lazyFetch(state.store); + auto storePath = nix::fetchToStore( + state.fetchSettings, + *state.store, + SourcePath(accessor), + FetchMode::Copy, + lockedRef.input.getName()); + state.allowPath(storePath); + return state.storePath(storePath); } else if (s.size() > 2 && s.at(0) == '<' && s.at(s.size() - 1) == '>') { diff --git a/src/libcmd/editor-for.cc b/src/libcmd/editor-for.cc index 6bf36bd647b..a5d635859a0 100644 --- a/src/libcmd/editor-for.cc +++ b/src/libcmd/editor-for.cc @@ -1,6 +1,6 @@ -#include "editor-for.hh" -#include "environment-variables.hh" -#include "source-path.hh" +#include "nix/cmd/editor-for.hh" +#include "nix/util/environment-variables.hh" +#include "nix/util/source-path.hh" namespace nix { diff --git a/src/libcmd/built-path.hh b/src/libcmd/include/nix/cmd/built-path.hh similarity index 97% rename from src/libcmd/built-path.hh rename to src/libcmd/include/nix/cmd/built-path.hh index dc78d3e599d..c885876a79d 100644 --- a/src/libcmd/built-path.hh +++ b/src/libcmd/include/nix/cmd/built-path.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "derived-path.hh" -#include "realisation.hh" +#include "nix/store/derived-path.hh" +#include "nix/store/realisation.hh" namespace nix { diff --git a/src/libcmd/command-installable-value.hh b/src/libcmd/include/nix/cmd/command-installable-value.hh similarity index 85% rename from src/libcmd/command-installable-value.hh rename to src/libcmd/include/nix/cmd/command-installable-value.hh index 7880d411998..b171d9f738d 100644 --- a/src/libcmd/command-installable-value.hh +++ b/src/libcmd/include/nix/cmd/command-installable-value.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "installable-value.hh" -#include "command.hh" +#include "nix/cmd/installable-value.hh" +#include "nix/cmd/command.hh" namespace nix { diff --git a/src/libcmd/command.hh b/src/libcmd/include/nix/cmd/command.hh similarity index 84% rename from src/libcmd/command.hh rename to src/libcmd/include/nix/cmd/command.hh index 23529848f6b..20cd1abc1c4 100644 --- a/src/libcmd/command.hh +++ b/src/libcmd/include/nix/cmd/command.hh @@ -1,11 +1,11 @@ #pragma once ///@file -#include "installable-value.hh" -#include "args.hh" -#include "common-eval-args.hh" -#include "path.hh" -#include "flake/lockfile.hh" +#include "nix/cmd/installable-value.hh" +#include "nix/util/args.hh" +#include "nix/cmd/common-eval-args.hh" +#include "nix/store/path.hh" +#include "nix/flake/lockfile.hh" #include @@ -18,7 +18,7 @@ extern char ** savedArgv; class EvalState; struct Pos; class Store; -class LocalFSStore; +struct LocalFSStore; static constexpr Command::Category catHelp = -1; static constexpr Command::Category catSecondary = 100; @@ -285,13 +285,16 @@ struct StorePathCommand : public StorePathsCommand struct RegisterCommand { typedef std::map, std::function()>> Commands; - static Commands * commands; + + static Commands & commands() + { + static Commands commands; + return commands; + } RegisterCommand(std::vector && name, std::function()> command) { - if (!commands) - commands = new Commands; - commands->emplace(name, command); + commands().emplace(name, command); } static nix::Commands getCommandsFor(const std::vector & prefix); @@ -333,7 +336,7 @@ struct MixEnvironment : virtual Args StringSet keepVars; StringSet unsetVars; - std::map setVars; + StringMap setVars; bool ignoreEnvironment; MixEnvironment(); @@ -347,7 +350,7 @@ struct MixEnvironment : virtual Args void setEnviron(); }; -void completeFlakeInputPath( +void completeFlakeInputAttrPath( AddCompletions & completions, ref evalState, const std::vector & flakeRefs, @@ -363,7 +366,7 @@ void completeFlakeRefWithFragment( const Strings & defaultFlakeAttrPaths, std::string_view prefix); -std::string showVersions(const std::set & versions); +std::string showVersions(const StringSet & versions); void printClosureDiff( ref store, const StorePath & beforePath, const StorePath & afterPath, std::string_view indent); @@ -374,4 +377,41 @@ void printClosureDiff( */ void createOutLinks(const std::filesystem::path & outLink, const BuiltPaths & buildables, LocalFSStore & store); -} +/** `outLink` parameter, `createOutLinksMaybe` method. See `MixOutLinkByDefault`. */ +struct MixOutLinkBase : virtual Args +{ + /** Prefix for any output symlinks. Empty means do not write an output symlink. */ + Path outLink; + + MixOutLinkBase(const std::string & defaultOutLink) + : outLink(defaultOutLink) + { + } + + void createOutLinksMaybe(const std::vector & buildables, ref & store); +}; + +/** `--out-link`, `--no-link`, `createOutLinksMaybe` */ +struct MixOutLinkByDefault : MixOutLinkBase, virtual Args +{ + MixOutLinkByDefault() + : MixOutLinkBase("result") + { + addFlag({ + .longName = "out-link", + .shortName = 'o', + .description = "Use *path* as prefix for the symlinks to the build results. It defaults to `result`.", + .labels = {"path"}, + .handler = {&outLink}, + .completer = completePath, + }); + + addFlag({ + .longName = "no-link", + .description = "Do not create symlinks to the build results.", + .handler = {&outLink, Path("")}, + }); + } +}; + +} // namespace nix diff --git a/src/libcmd/common-eval-args.hh b/src/libcmd/include/nix/cmd/common-eval-args.hh similarity index 80% rename from src/libcmd/common-eval-args.hh rename to src/libcmd/include/nix/cmd/common-eval-args.hh index c62365b32e2..88ede1ed7e7 100644 --- a/src/libcmd/common-eval-args.hh +++ b/src/libcmd/include/nix/cmd/common-eval-args.hh @@ -1,10 +1,11 @@ #pragma once ///@file -#include "args.hh" -#include "canon-path.hh" -#include "common-args.hh" -#include "search-path.hh" +#include "nix/util/args.hh" +#include "nix/util/canon-path.hh" +#include "nix/main/common-args.hh" +#include "nix/expr/search-path.hh" +#include "nix/expr/eval-settings.hh" #include @@ -15,25 +16,23 @@ class Store; namespace fetchers { struct Settings; } class EvalState; -struct EvalSettings; struct CompatibilitySettings; class Bindings; -struct SourcePath; namespace flake { struct Settings; } /** - * @todo Get rid of global setttings variables + * @todo Get rid of global settings variables */ extern fetchers::Settings fetchSettings; /** - * @todo Get rid of global setttings variables + * @todo Get rid of global settings variables */ extern EvalSettings evalSettings; /** - * @todo Get rid of global setttings variables + * @todo Get rid of global settings variables */ extern flake::Settings flakeSettings; diff --git a/src/libcmd/compatibility-settings.hh b/src/libcmd/include/nix/cmd/compatibility-settings.hh similarity index 97% rename from src/libcmd/compatibility-settings.hh rename to src/libcmd/include/nix/cmd/compatibility-settings.hh index a129a957a64..c7061a0a14d 100644 --- a/src/libcmd/compatibility-settings.hh +++ b/src/libcmd/include/nix/cmd/compatibility-settings.hh @@ -1,5 +1,5 @@ #pragma once -#include "config.hh" +#include "nix/util/configuration.hh" namespace nix { struct CompatibilitySettings : public Config diff --git a/src/libcmd/editor-for.hh b/src/libcmd/include/nix/cmd/editor-for.hh similarity index 74% rename from src/libcmd/editor-for.hh rename to src/libcmd/include/nix/cmd/editor-for.hh index 8acd7011e69..11414e82382 100644 --- a/src/libcmd/editor-for.hh +++ b/src/libcmd/include/nix/cmd/editor-for.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "types.hh" -#include "source-path.hh" +#include "nix/util/types.hh" +#include "nix/util/source-path.hh" namespace nix { diff --git a/src/libcmd/installable-attr-path.hh b/src/libcmd/include/nix/cmd/installable-attr-path.hh similarity index 61% rename from src/libcmd/installable-attr-path.hh rename to src/libcmd/include/nix/cmd/installable-attr-path.hh index 86c2f82192c..5a0dc993c9f 100644 --- a/src/libcmd/installable-attr-path.hh +++ b/src/libcmd/include/nix/cmd/installable-attr-path.hh @@ -1,22 +1,22 @@ #pragma once ///@file -#include "globals.hh" -#include "installable-value.hh" -#include "outputs-spec.hh" -#include "command.hh" -#include "attr-path.hh" -#include "common-eval-args.hh" -#include "derivations.hh" -#include "eval-inline.hh" -#include "eval.hh" -#include "get-drvs.hh" -#include "store-api.hh" -#include "shared.hh" -#include "eval-cache.hh" -#include "url.hh" -#include "registry.hh" -#include "build-result.hh" +#include "nix/store/globals.hh" +#include "nix/cmd/installable-value.hh" +#include "nix/store/outputs-spec.hh" +#include "nix/cmd/command.hh" +#include "nix/expr/attr-path.hh" +#include "nix/cmd/common-eval-args.hh" +#include "nix/store/derivations.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/get-drvs.hh" +#include "nix/store/store-api.hh" +#include "nix/main/shared.hh" +#include "nix/expr/eval-cache.hh" +#include "nix/util/url.hh" +#include "nix/fetchers/registry.hh" +#include "nix/store/build-result.hh" #include #include diff --git a/src/libcmd/installable-derived-path.hh b/src/libcmd/include/nix/cmd/installable-derived-path.hh similarity index 94% rename from src/libcmd/installable-derived-path.hh rename to src/libcmd/include/nix/cmd/installable-derived-path.hh index e0b4f18b38b..daa6ba86867 100644 --- a/src/libcmd/installable-derived-path.hh +++ b/src/libcmd/include/nix/cmd/installable-derived-path.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "installables.hh" +#include "nix/cmd/installables.hh" namespace nix { diff --git a/src/libcmd/installable-flake.hh b/src/libcmd/include/nix/cmd/installable-flake.hh similarity index 96% rename from src/libcmd/installable-flake.hh rename to src/libcmd/include/nix/cmd/installable-flake.hh index 212403dd42c..8699031b5b5 100644 --- a/src/libcmd/installable-flake.hh +++ b/src/libcmd/include/nix/cmd/installable-flake.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "common-eval-args.hh" -#include "installable-value.hh" +#include "nix/cmd/common-eval-args.hh" +#include "nix/cmd/installable-value.hh" namespace nix { diff --git a/src/libcmd/installable-value.hh b/src/libcmd/include/nix/cmd/installable-value.hh similarity index 95% rename from src/libcmd/installable-value.hh rename to src/libcmd/include/nix/cmd/installable-value.hh index 4b6dbd306aa..e65c199a505 100644 --- a/src/libcmd/installable-value.hh +++ b/src/libcmd/include/nix/cmd/installable-value.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "installables.hh" -#include "flake/flake.hh" +#include "nix/cmd/installables.hh" +#include "nix/flake/flake.hh" namespace nix { @@ -21,6 +21,7 @@ struct App struct UnresolvedApp { App unresolved; + std::vector build(ref evalStore, ref store); App resolve(ref evalStore, ref store); }; diff --git a/src/libcmd/installables.hh b/src/libcmd/include/nix/cmd/installables.hh similarity index 95% rename from src/libcmd/installables.hh rename to src/libcmd/include/nix/cmd/installables.hh index c995c3019f4..84941278a44 100644 --- a/src/libcmd/installables.hh +++ b/src/libcmd/include/nix/cmd/installables.hh @@ -1,12 +1,12 @@ #pragma once ///@file -#include "path.hh" -#include "outputs-spec.hh" -#include "derived-path.hh" -#include "built-path.hh" -#include "store-api.hh" -#include "build-result.hh" +#include "nix/store/path.hh" +#include "nix/store/outputs-spec.hh" +#include "nix/store/derived-path.hh" +#include "nix/cmd/built-path.hh" +#include "nix/store/store-api.hh" +#include "nix/store/build-result.hh" #include diff --git a/src/libcmd/legacy.hh b/src/libcmd/include/nix/cmd/legacy.hh similarity index 71% rename from src/libcmd/legacy.hh rename to src/libcmd/include/nix/cmd/legacy.hh index 357500a4dc2..0c375a7d2a1 100644 --- a/src/libcmd/legacy.hh +++ b/src/libcmd/include/nix/cmd/legacy.hh @@ -12,12 +12,15 @@ typedef std::function MainFunction; struct RegisterLegacyCommand { typedef std::map Commands; - static Commands * commands; + + static Commands & commands() { + static Commands commands; + return commands; + } RegisterLegacyCommand(const std::string & name, MainFunction fun) { - if (!commands) commands = new Commands; - (*commands)[name] = fun; + commands()[name] = fun; } }; diff --git a/src/libcmd/markdown.hh b/src/libcmd/include/nix/cmd/markdown.hh similarity index 100% rename from src/libcmd/markdown.hh rename to src/libcmd/include/nix/cmd/markdown.hh diff --git a/src/libcmd/include/nix/cmd/meson.build b/src/libcmd/include/nix/cmd/meson.build new file mode 100644 index 00000000000..368edb28e5b --- /dev/null +++ b/src/libcmd/include/nix/cmd/meson.build @@ -0,0 +1,23 @@ +# Public headers directory + +include_dirs = [include_directories('../..')] + +headers = files( + 'built-path.hh', + 'command-installable-value.hh', + 'command.hh', + 'common-eval-args.hh', + 'compatibility-settings.hh', + 'editor-for.hh', + 'installable-attr-path.hh', + 'installable-derived-path.hh', + 'installable-flake.hh', + 'installable-value.hh', + 'installables.hh', + 'legacy.hh', + 'markdown.hh', + 'misc-store-flags.hh', + 'network-proxy.hh', + 'repl-interacter.hh', + 'repl.hh', +) diff --git a/src/libcmd/misc-store-flags.hh b/src/libcmd/include/nix/cmd/misc-store-flags.hh similarity index 90% rename from src/libcmd/misc-store-flags.hh rename to src/libcmd/include/nix/cmd/misc-store-flags.hh index 124372af78c..c9467ad8e3a 100644 --- a/src/libcmd/misc-store-flags.hh +++ b/src/libcmd/include/nix/cmd/misc-store-flags.hh @@ -1,5 +1,5 @@ -#include "args.hh" -#include "content-address.hh" +#include "nix/util/args.hh" +#include "nix/store/content-address.hh" namespace nix::flag { diff --git a/src/libcmd/network-proxy.hh b/src/libcmd/include/nix/cmd/network-proxy.hh similarity index 93% rename from src/libcmd/network-proxy.hh rename to src/libcmd/include/nix/cmd/network-proxy.hh index 0b6856acbf4..255597a6109 100644 --- a/src/libcmd/network-proxy.hh +++ b/src/libcmd/include/nix/cmd/network-proxy.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "types.hh" +#include "nix/util/types.hh" namespace nix { diff --git a/src/libcmd/repl-interacter.hh b/src/libcmd/include/nix/cmd/repl-interacter.hh similarity index 94% rename from src/libcmd/repl-interacter.hh rename to src/libcmd/include/nix/cmd/repl-interacter.hh index cc70efd0729..eb58563b2ec 100644 --- a/src/libcmd/repl-interacter.hh +++ b/src/libcmd/include/nix/cmd/repl-interacter.hh @@ -1,8 +1,8 @@ #pragma once /// @file -#include "finally.hh" -#include "types.hh" +#include "nix/util/finally.hh" +#include "nix/util/types.hh" #include #include diff --git a/src/libcmd/repl.hh b/src/libcmd/include/nix/cmd/repl.hh similarity index 97% rename from src/libcmd/repl.hh rename to src/libcmd/include/nix/cmd/repl.hh index 11d1820f504..83e39727f81 100644 --- a/src/libcmd/repl.hh +++ b/src/libcmd/include/nix/cmd/repl.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "eval.hh" +#include "nix/expr/eval.hh" namespace nix { diff --git a/src/libcmd/installable-attr-path.cc b/src/libcmd/installable-attr-path.cc index 8917e7a018a..7783b4f40da 100644 --- a/src/libcmd/installable-attr-path.cc +++ b/src/libcmd/installable-attr-path.cc @@ -1,21 +1,21 @@ -#include "globals.hh" -#include "installable-attr-path.hh" -#include "outputs-spec.hh" -#include "util.hh" -#include "command.hh" -#include "attr-path.hh" -#include "common-eval-args.hh" -#include "derivations.hh" -#include "eval-inline.hh" -#include "eval.hh" -#include "get-drvs.hh" -#include "store-api.hh" -#include "shared.hh" -#include "flake/flake.hh" -#include "eval-cache.hh" -#include "url.hh" -#include "registry.hh" -#include "build-result.hh" +#include "nix/store/globals.hh" +#include "nix/cmd/installable-attr-path.hh" +#include "nix/store/outputs-spec.hh" +#include "nix/util/util.hh" +#include "nix/cmd/command.hh" +#include "nix/expr/attr-path.hh" +#include "nix/cmd/common-eval-args.hh" +#include "nix/store/derivations.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/get-drvs.hh" +#include "nix/store/store-api.hh" +#include "nix/main/shared.hh" +#include "nix/flake/flake.hh" +#include "nix/expr/eval-cache.hh" +#include "nix/util/url.hh" +#include "nix/fetchers/registry.hh" +#include "nix/store/build-result.hh" #include #include @@ -72,7 +72,7 @@ DerivedPathsWithInfo InstallableAttrPath::toDerivedPaths() auto newOutputs = std::visit(overloaded { [&](const ExtendedOutputsSpec::Default & d) -> OutputsSpec { - std::set outputsToInstall; + StringSet outputsToInstall; for (auto & output : packageInfo.queryOutputs(false, true)) outputsToInstall.insert(output.first); if (outputsToInstall.empty()) diff --git a/src/libcmd/installable-derived-path.cc b/src/libcmd/installable-derived-path.cc index abacd73502c..5a92f81c7d4 100644 --- a/src/libcmd/installable-derived-path.cc +++ b/src/libcmd/installable-derived-path.cc @@ -1,5 +1,5 @@ -#include "installable-derived-path.hh" -#include "derivations.hh" +#include "nix/cmd/installable-derived-path.hh" +#include "nix/store/derivations.hh" namespace nix { diff --git a/src/libcmd/installable-flake.cc b/src/libcmd/installable-flake.cc index 6c9ee674808..85a4188a7d7 100644 --- a/src/libcmd/installable-flake.cc +++ b/src/libcmd/installable-flake.cc @@ -1,22 +1,22 @@ -#include "globals.hh" -#include "installable-flake.hh" -#include "installable-derived-path.hh" -#include "outputs-spec.hh" -#include "util.hh" -#include "command.hh" -#include "attr-path.hh" -#include "common-eval-args.hh" -#include "derivations.hh" -#include "eval-inline.hh" -#include "eval.hh" -#include "get-drvs.hh" -#include "store-api.hh" -#include "shared.hh" -#include "flake/flake.hh" -#include "eval-cache.hh" -#include "url.hh" -#include "registry.hh" -#include "build-result.hh" +#include "nix/store/globals.hh" +#include "nix/cmd/installable-flake.hh" +#include "nix/cmd/installable-derived-path.hh" +#include "nix/store/outputs-spec.hh" +#include "nix/util/util.hh" +#include "nix/cmd/command.hh" +#include "nix/expr/attr-path.hh" +#include "nix/cmd/common-eval-args.hh" +#include "nix/store/derivations.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/get-drvs.hh" +#include "nix/store/store-api.hh" +#include "nix/main/shared.hh" +#include "nix/flake/flake.hh" +#include "nix/expr/eval-cache.hh" +#include "nix/util/url.hh" +#include "nix/fetchers/registry.hh" +#include "nix/store/build-result.hh" #include #include @@ -117,7 +117,7 @@ DerivedPathsWithInfo InstallableFlake::toDerivedPaths() .drvPath = makeConstantStorePathRef(std::move(drvPath)), .outputs = std::visit(overloaded { [&](const ExtendedOutputsSpec::Default & d) -> OutputsSpec { - std::set outputsToInstall; + StringSet outputsToInstall; if (auto aOutputSpecified = attr->maybeGetAttr(state->sOutputSpecified)) { if (aOutputSpecified->getBool()) { if (auto aOutputName = attr->maybeGetAttr("outputName")) diff --git a/src/libcmd/installable-value.cc b/src/libcmd/installable-value.cc index 1aa2e65c1e5..e92496347e0 100644 --- a/src/libcmd/installable-value.cc +++ b/src/libcmd/installable-value.cc @@ -1,6 +1,6 @@ -#include "installable-value.hh" -#include "eval-cache.hh" -#include "fetch-to-store.hh" +#include "nix/cmd/installable-value.hh" +#include "nix/expr/eval-cache.hh" +#include "nix/fetchers/fetch-to-store.hh" namespace nix { @@ -45,7 +45,7 @@ ref InstallableValue::require(ref installable) std::optional InstallableValue::trySinglePathToDerivedPaths(Value & v, const PosIdx pos, std::string_view errorCtx) { if (v.type() == nPath) { - auto storePath = fetchToStore(*state->store, v.path(), FetchMode::Copy); + auto storePath = fetchToStore(state->fetchSettings, *state->store, v.path(), FetchMode::Copy); return {{ .path = DerivedPath::Opaque { .path = std::move(storePath), diff --git a/src/libcmd/installables.cc b/src/libcmd/installables.cc index 250cd141399..49ffd82e1a3 100644 --- a/src/libcmd/installables.cc +++ b/src/libcmd/installables.cc @@ -1,46 +1,44 @@ -#include "globals.hh" -#include "installables.hh" -#include "installable-derived-path.hh" -#include "installable-attr-path.hh" -#include "installable-flake.hh" -#include "outputs-spec.hh" -#include "users.hh" -#include "util.hh" -#include "command.hh" -#include "attr-path.hh" -#include "common-eval-args.hh" -#include "derivations.hh" -#include "eval-inline.hh" -#include "eval.hh" -#include "eval-settings.hh" -#include "get-drvs.hh" -#include "store-api.hh" -#include "shared.hh" -#include "flake/flake.hh" -#include "eval-cache.hh" -#include "url.hh" -#include "registry.hh" -#include "build-result.hh" +#include "nix/store/globals.hh" +#include "nix/cmd/installables.hh" +#include "nix/cmd/installable-derived-path.hh" +#include "nix/cmd/installable-attr-path.hh" +#include "nix/cmd/installable-flake.hh" +#include "nix/store/outputs-spec.hh" +#include "nix/util/users.hh" +#include "nix/util/util.hh" +#include "nix/cmd/command.hh" +#include "nix/expr/attr-path.hh" +#include "nix/cmd/common-eval-args.hh" +#include "nix/store/derivations.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/eval-settings.hh" +#include "nix/expr/get-drvs.hh" +#include "nix/store/store-api.hh" +#include "nix/main/shared.hh" +#include "nix/flake/flake.hh" +#include "nix/expr/eval-cache.hh" +#include "nix/util/url.hh" +#include "nix/fetchers/registry.hh" +#include "nix/store/build-result.hh" #include #include #include -#include "strings-inline.hh" +#include "nix/util/strings-inline.hh" namespace nix { -namespace fs { using namespace std::filesystem; } - -void completeFlakeInputPath( +void completeFlakeInputAttrPath( AddCompletions & completions, ref evalState, const std::vector & flakeRefs, std::string_view prefix) { for (auto & flakeRef : flakeRefs) { - auto flake = flake::getFlake(*evalState, flakeRef, true); + auto flake = flake::getFlake(*evalState, flakeRef, fetchers::UseRegistries::All); for (auto & input : flake.inputs) if (hasPrefix(input.first, prefix)) completions.add(input.first); @@ -64,21 +62,21 @@ MixFlakeOptions::MixFlakeOptions() .handler = {[&]() { lockFlags.recreateLockFile = true; warn("'--recreate-lock-file' is deprecated and will be removed in a future version; use 'nix flake update' instead."); - }} + }}, }); addFlag({ .longName = "no-update-lock-file", .description = "Do not allow any updates to the flake's lock file.", .category = category, - .handler = {&lockFlags.updateLockFile, false} + .handler = {&lockFlags.updateLockFile, false}, }); addFlag({ .longName = "no-write-lock-file", .description = "Do not write the flake's newly generated lock file.", .category = category, - .handler = {&lockFlags.writeLockFile, false} + .handler = {&lockFlags.writeLockFile, false}, }); addFlag({ @@ -94,14 +92,14 @@ MixFlakeOptions::MixFlakeOptions() .handler = {[&]() { lockFlags.useRegistries = false; warn("'--no-registries' is deprecated; use '--no-use-registries'"); - }} + }}, }); addFlag({ .longName = "commit-lock-file", .description = "Commit changes to the flake's lock file.", .category = category, - .handler = {&lockFlags.commitLockFile, true} + .handler = {&lockFlags.commitLockFile, true}, }); addFlag({ @@ -117,11 +115,11 @@ MixFlakeOptions::MixFlakeOptions() .labels = {"input-path"}, .handler = {[&](std::string s) { warn("'--update-input' is a deprecated alias for 'flake update' and will be removed in a future version."); - lockFlags.inputUpdates.insert(flake::parseInputPath(s)); + lockFlags.inputUpdates.insert(flake::parseInputAttrPath(s)); }}, .completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) { - completeFlakeInputPath(completions, getEvalState(), getFlakeRefsForCompletion(), prefix); - }} + completeFlakeInputAttrPath(completions, getEvalState(), getFlakeRefsForCompletion(), prefix); + }}, }); addFlag({ @@ -129,19 +127,19 @@ MixFlakeOptions::MixFlakeOptions() .description = "Override a specific flake input (e.g. `dwarffs/nixpkgs`). This implies `--no-write-lock-file`.", .category = category, .labels = {"input-path", "flake-url"}, - .handler = {[&](std::string inputPath, std::string flakeRef) { + .handler = {[&](std::string inputAttrPath, std::string flakeRef) { lockFlags.writeLockFile = false; lockFlags.inputOverrides.insert_or_assign( - flake::parseInputPath(inputPath), + flake::parseInputAttrPath(inputAttrPath), parseFlakeRef(fetchSettings, flakeRef, absPath(getCommandBaseDir()), true)); }}, .completer = {[&](AddCompletions & completions, size_t n, std::string_view prefix) { if (n == 0) { - completeFlakeInputPath(completions, getEvalState(), getFlakeRefsForCompletion(), prefix); + completeFlakeInputAttrPath(completions, getEvalState(), getFlakeRefsForCompletion(), prefix); } else if (n == 1) { completeFlakeRef(completions, getEvalState()->store, prefix); } - }} + }}, }); addFlag({ @@ -152,7 +150,7 @@ MixFlakeOptions::MixFlakeOptions() .handler = {[&](std::string lockFilePath) { lockFlags.referenceLockFilePath = {getFSSourceAccessor(), CanonPath(absPath(lockFilePath))}; }}, - .completer = completePath + .completer = completePath, }); addFlag({ @@ -163,7 +161,7 @@ MixFlakeOptions::MixFlakeOptions() .handler = {[&](std::string lockFilePath) { lockFlags.outputLockFilePath = lockFilePath; }}, - .completer = completePath + .completer = completePath, }); addFlag({ @@ -190,7 +188,7 @@ MixFlakeOptions::MixFlakeOptions() }}, .completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) { completeFlakeRef(completions, getEvalState()->store, prefix); - }} + }}, }); } @@ -201,12 +199,12 @@ SourceExprCommand::SourceExprCommand() .shortName = 'f', .description = "Interpret [*installables*](@docroot@/command-ref/new-cli/nix.md#installables) as attribute paths relative to the Nix expression stored in *file*. " - "If *file* is the character -, then a Nix expression will be read from standard input. " + "If *file* is the character -, then a Nix expression is read from standard input. " "Implies `--impure`.", .category = installablesCategory, .labels = {"file"}, .handler = {&file}, - .completer = completePath + .completer = completePath, }); addFlag({ @@ -214,7 +212,7 @@ SourceExprCommand::SourceExprCommand() .description = "Interpret [*installables*](@docroot@/command-ref/new-cli/nix.md#installables) as attribute paths relative to the Nix expression *expr*.", .category = installablesCategory, .labels = {"expr"}, - .handler = {&expr} + .handler = {&expr}, }); } @@ -343,7 +341,7 @@ void completeFlakeRefWithFragment( auto flakeRefS = std::string(prefix.substr(0, hash)); // TODO: ideally this would use the command base directory instead of assuming ".". - auto flakeRef = parseFlakeRef(fetchSettings, expandTilde(flakeRefS), fs::current_path().string()); + auto flakeRef = parseFlakeRef(fetchSettings, expandTilde(flakeRefS), std::filesystem::current_path().string()); auto evalCache = openEvalCache(*evalState, std::make_shared(lockFlake( @@ -450,7 +448,7 @@ ref openEvalCache( std::shared_ptr lockedFlake) { auto fingerprint = evalSettings.useEvalCache && evalSettings.pureEval - ? lockedFlake->getFingerprint(state.store) + ? lockedFlake->getFingerprint(state.store, state.fetchSettings) : std::nullopt; auto rootLoader = [&state, lockedFlake]() { @@ -491,7 +489,11 @@ Installables SourceExprCommand::parseInstallables( throw UsageError("'--file' and '--expr' are exclusive"); // FIXME: backward compatibility hack - if (file) evalSettings.pureEval = false; + if (file) { + if (evalSettings.pureEval && evalSettings.pureEval.overridden) + throw UsageError("'--file' is not compatible with '--pure-eval'"); + evalSettings.pureEval = false; + } auto state = getEvalState(); auto vFile = state->allocValue(); @@ -834,7 +836,7 @@ RawInstallablesCommand::RawInstallablesCommand() addFlag({ .longName = "stdin", .description = "Read installables from the standard input. No default installable applied.", - .handler = {&readFromStdIn, true} + .handler = {&readFromStdIn, true}, }); expectArgs({ @@ -847,7 +849,7 @@ RawInstallablesCommand::RawInstallablesCommand() void RawInstallablesCommand::applyDefaultInstallables(std::vector & rawInstallables) { if (rawInstallables.empty()) { - // FIXME: commands like "nix profile install" should not have a + // FIXME: commands like "nix profile add" should not have a // default, probably. rawInstallables.push_back("."); } diff --git a/src/libcmd/legacy.cc b/src/libcmd/legacy.cc deleted file mode 100644 index 6df09ee37a5..00000000000 --- a/src/libcmd/legacy.cc +++ /dev/null @@ -1,7 +0,0 @@ -#include "legacy.hh" - -namespace nix { - -RegisterLegacyCommand::Commands * RegisterLegacyCommand::commands = 0; - -} diff --git a/src/libcmd/markdown.cc b/src/libcmd/markdown.cc index 6a0d05d9fba..41da73c7af8 100644 --- a/src/libcmd/markdown.cc +++ b/src/libcmd/markdown.cc @@ -1,8 +1,10 @@ -#include "markdown.hh" -#include "environment-variables.hh" -#include "error.hh" -#include "finally.hh" -#include "terminal.hh" +#include "nix/cmd/markdown.hh" +#include "nix/util/environment-variables.hh" +#include "nix/util/error.hh" +#include "nix/util/finally.hh" +#include "nix/util/terminal.hh" + +#include "cmd-config-private.hh" #if HAVE_LOWDOWN # include @@ -16,13 +18,25 @@ static std::string doRenderMarkdownToTerminal(std::string_view markdown) { int windowWidth = getWindowSize().second; +#if HAVE_LOWDOWN_1_4 + struct lowdown_opts_term opts_term { + .cols = (size_t) std::max(windowWidth - 5, 60), + .hmargin = 0, + .vmargin = 0, + }; +#endif struct lowdown_opts opts { .type = LOWDOWN_TERM, +#if HAVE_LOWDOWN_1_4 + .term = opts_term, +#endif .maxdepth = 20, +#if !HAVE_LOWDOWN_1_4 .cols = (size_t) std::max(windowWidth - 5, 60), .hmargin = 0, .vmargin = 0, +#endif .feat = LOWDOWN_COMMONMARK | LOWDOWN_FENCED | LOWDOWN_DEFLIST | LOWDOWN_TABLES, .oflags = LOWDOWN_TERM_NOLINK, }; diff --git a/src/libcmd/meson.build b/src/libcmd/meson.build index 1f27c161402..216d4df9c8d 100644 --- a/src/libcmd/meson.build +++ b/src/libcmd/meson.build @@ -4,8 +4,6 @@ project('nix-cmd', 'cpp', 'cpp_std=c++2a', # TODO(Qyriad): increase the warning level 'warning_level=1', - 'debug=true', - 'optimization=2', 'errorlogs=true', # Please print logs for tests that fail ], meson_version : '>= 1.1', @@ -14,7 +12,7 @@ project('nix-cmd', 'cpp', cxx = meson.get_compiler('cpp') -subdir('build-utils-meson/deps-lists') +subdir('nix-meson-build-support/deps-lists') configdata = configuration_data() @@ -28,7 +26,7 @@ deps_public_maybe_subproject = [ dependency('nix-flake'), dependency('nix-main'), ] -subdir('build-utils-meson/subprojects') +subdir('nix-meson-build-support/subprojects') nlohmann_json = dependency('nlohmann_json', version : '>= 3.9') deps_public += nlohmann_json @@ -36,6 +34,8 @@ deps_public += nlohmann_json lowdown = dependency('lowdown', version : '>= 0.9.0', required : get_option('markdown')) deps_private += lowdown configdata.set('HAVE_LOWDOWN', lowdown.found().to_int()) +# The API changed slightly around terminal initialization. +configdata.set('HAVE_LOWDOWN_1_4', lowdown.version().version_compare('>= 1.4.0').to_int()) readline_flavor = get_option('readline-flavor') if readline_flavor == 'editline' @@ -44,33 +44,21 @@ if readline_flavor == 'editline' elif readline_flavor == 'readline' readline = dependency('readline') deps_private += readline - configdata.set( - 'USE_READLINE', - 1, - description: 'Use readline instead of editline', - ) else error('illegal editline flavor', readline_flavor) endif - -config_h = configure_file( - configuration : configdata, - output : 'config-cmd.hh', +configdata.set( + 'USE_READLINE', + (readline_flavor == 'readline').to_int(), + description: 'Use readline instead of editline', ) -add_project_arguments( - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. - '-include', 'config-util.hh', - '-include', 'config-store.hh', - # '-include', 'config-fetchers.h', - '-include', 'config-expr.hh', - '-include', 'config-main.hh', - '-include', 'config-cmd.hh', - language : 'cpp', +config_priv_h = configure_file( + configuration : configdata, + output : 'cmd-config-private.hh', ) -subdir('build-utils-meson/common') +subdir('nix-meson-build-support/common') sources = files( 'built-path.cc', @@ -83,7 +71,6 @@ sources = files( 'installable-flake.cc', 'installable-value.cc', 'installables.cc', - 'legacy.cc', 'markdown.cc', 'misc-store-flags.cc', 'network-proxy.cc', @@ -91,38 +78,24 @@ sources = files( 'repl.cc', ) -include_dirs = [include_directories('.')] - -headers = [config_h] + files( - 'built-path.hh', - 'command-installable-value.hh', - 'command.hh', - 'common-eval-args.hh', - 'compatibility-settings.hh', - 'editor-for.hh', - 'installable-attr-path.hh', - 'installable-derived-path.hh', - 'installable-flake.hh', - 'installable-value.hh', - 'installables.hh', - 'legacy.hh', - 'markdown.hh', - 'misc-store-flags.hh', - 'network-proxy.hh', - 'repl-interacter.hh', - 'repl.hh', -) +subdir('include/nix/cmd') + +subdir('nix-meson-build-support/export-all-symbols') +subdir('nix-meson-build-support/windows-version') this_library = library( 'nixcmd', sources, + config_priv_h, dependencies : deps_public + deps_private + deps_other, + include_directories : include_dirs, + link_args: linker_export_flags, prelink : true, # For C++ static initializers install : true, ) -install_headers(headers, subdir : 'nix', preserve_path : true) +install_headers(headers, subdir : 'nix/cmd', preserve_path : true) libraries_private = [] -subdir('build-utils-meson/export') +subdir('nix-meson-build-support/export') diff --git a/src/libcmd/misc-store-flags.cc b/src/libcmd/misc-store-flags.cc index 06552c03223..a57ad35ffb3 100644 --- a/src/libcmd/misc-store-flags.cc +++ b/src/libcmd/misc-store-flags.cc @@ -1,4 +1,4 @@ -#include "misc-store-flags.hh" +#include "nix/cmd/misc-store-flags.hh" namespace nix::flag { @@ -50,7 +50,7 @@ Args::Flag hashAlgo(std::string && longName, HashAlgorithm * ha) { return Args::Flag { .longName = std::move(longName), - .description = "Hash algorithm (`md5`, `sha1`, `sha256`, or `sha512`).", + .description = "Hash algorithm (`blake3`, `md5`, `sha1`, `sha256`, or `sha512`).", .labels = {"hash-algo"}, .handler = {[ha](std::string s) { *ha = parseHashAlgo(s); @@ -63,7 +63,7 @@ Args::Flag hashAlgoOpt(std::string && longName, std::optional * o { return Args::Flag { .longName = std::move(longName), - .description = "Hash algorithm (`md5`, `sha1`, `sha256`, or `sha512`). Can be omitted for SRI hashes.", + .description = "Hash algorithm (`blake3`, `md5`, `sha1`, `sha256`, or `sha512`). Can be omitted for SRI hashes.", .labels = {"hash-algo"}, .handler = {[oha](std::string s) { *oha = std::optional{parseHashAlgo(s)}; @@ -120,7 +120,7 @@ Args::Flag contentAddressMethod(ContentAddressMethod * method) - [`text`](@docroot@/store/store-object/content-address.md#method-text): Like `flat`, but used for - [derivations](@docroot@/glossary.md#store-derivation) serialized in store object and + [derivations](@docroot@/glossary.md#gloss-store-derivation) serialized in store object and [`builtins.toFile`](@docroot@/language/builtins.html#builtins-toFile). For advanced use-cases only; for regular usage prefer `nar` and `flat`. diff --git a/src/libcmd/network-proxy.cc b/src/libcmd/network-proxy.cc index 738bf614729..a4a89685c4d 100644 --- a/src/libcmd/network-proxy.cc +++ b/src/libcmd/network-proxy.cc @@ -1,8 +1,8 @@ -#include "network-proxy.hh" +#include "nix/cmd/network-proxy.hh" #include -#include "environment-variables.hh" +#include "nix/util/environment-variables.hh" namespace nix { diff --git a/src/libcmd/nix-meson-build-support b/src/libcmd/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/libcmd/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/libcmd/package.nix b/src/libcmd/package.nix index 244179ee4f7..be5054f6403 100644 --- a/src/libcmd/package.nix +++ b/src/libcmd/package.nix @@ -1,32 +1,33 @@ -{ lib -, stdenv -, mkMesonLibrary +{ + lib, + stdenv, + mkMesonLibrary, -, nix-util -, nix-store -, nix-fetchers -, nix-expr -, nix-flake -, nix-main -, editline -, readline -, lowdown -, nlohmann_json + nix-util, + nix-store, + nix-fetchers, + nix-expr, + nix-flake, + nix-main, + editline, + readline, + lowdown, + nlohmann_json, -# Configuration Options + # Configuration Options -, version + version, -# Whether to enable Markdown rendering in the Nix binary. -, enableMarkdown ? !stdenv.hostPlatform.isWindows + # Whether to enable Markdown rendering in the Nix binary. + enableMarkdown ? !stdenv.hostPlatform.isWindows, -# Which interactive line editor library to use for Nix's repl. -# -# Currently supported choices are: -# -# - editline (default) -# - readline -, readlineFlavor ? if stdenv.hostPlatform.isWindows then "readline" else "editline" + # Which interactive line editor library to use for Nix's repl. + # + # Currently supported choices are: + # + # - editline (default) + # - readline + readlineFlavor ? if stdenv.hostPlatform.isWindows then "readline" else "editline", }: let @@ -39,12 +40,13 @@ mkMesonLibrary (finalAttrs: { workDir = ./.; fileset = fileset.unions [ - ../../build-utils-meson - ./build-utils-meson + ../../nix-meson-build-support + ./nix-meson-build-support ../../.version ./.version ./meson.build ./meson.options + ./include/nix/cmd/meson.build (fileset.fileFilter (file: file.hasExt "cc") ./.) (fileset.fileFilter (file: file.hasExt "hh") ./.) ]; @@ -63,23 +65,11 @@ mkMesonLibrary (finalAttrs: { nlohmann_json ]; - preConfigure = - # "Inline" .version so it's not a symlink, and includes the suffix. - # Do the meson utils, without modification. - '' - chmod u+w ./.version - echo ${version} > ../../.version - ''; - mesonFlags = [ (lib.mesonEnable "markdown" enableMarkdown) (lib.mesonOption "readline-flavor" readlineFlavor) ]; - env = lib.optionalAttrs (stdenv.isLinux && !(stdenv.hostPlatform.isStatic && stdenv.system == "aarch64-linux")) { - LDFLAGS = "-fuse-ld=gold"; - }; - meta = { platforms = lib.platforms.unix ++ lib.platforms.windows; }; diff --git a/src/libcmd/repl-interacter.cc b/src/libcmd/repl-interacter.cc index 187af46eaa4..4de335dd5e5 100644 --- a/src/libcmd/repl-interacter.cc +++ b/src/libcmd/repl-interacter.cc @@ -1,6 +1,10 @@ +#include "cmd-config-private.hh" + #include -#ifdef USE_READLINE +#include + +#if USE_READLINE #include #include #else @@ -14,12 +18,12 @@ extern "C" { } #endif -#include "signals.hh" -#include "finally.hh" -#include "repl-interacter.hh" -#include "file-system.hh" -#include "repl.hh" -#include "environment-variables.hh" +#include "nix/util/signals.hh" +#include "nix/util/finally.hh" +#include "nix/cmd/repl-interacter.hh" +#include "nix/util/file-system.hh" +#include "nix/cmd/repl.hh" +#include "nix/util/environment-variables.hh" namespace nix { @@ -35,7 +39,7 @@ void sigintHandler(int signo) static detail::ReplCompleterMixin * curRepl; // ugly -#ifndef USE_READLINE +#if !USE_READLINE static char * completionCallback(char * s, int * match) { auto possible = curRepl->completePrefix(s); @@ -113,14 +117,14 @@ ReadlineLikeInteracter::Guard ReadlineLikeInteracter::init(detail::ReplCompleter } catch (SystemError & e) { logWarning(e.info()); } -#ifndef USE_READLINE +#if !USE_READLINE el_hist_size = 1000; #endif read_history(historyFile.c_str()); auto oldRepl = curRepl; curRepl = repl; Guard restoreRepl([oldRepl] { curRepl = oldRepl; }); -#ifndef USE_READLINE +#if !USE_READLINE rl_set_complete_func(completionCallback); rl_set_list_possib_func(listPossibleCallback); #endif @@ -133,7 +137,7 @@ static constexpr const char * promptForType(ReplPromptType promptType) case ReplPromptType::ReplPrompt: return "nix-repl> "; case ReplPromptType::ContinuationPrompt: - return " "; + return " > "; // 9 spaces + > } assert(false); } @@ -183,7 +187,7 @@ bool ReadlineLikeInteracter::getLine(std::string & input, ReplPromptType promptT // quite useful for reading the test output, so we add it here. if (auto e = getEnv("_NIX_TEST_REPL_ECHO"); s && e && *e == "1") { -#ifndef USE_READLINE +#if !USE_READLINE // This is probably not right for multi-line input, but we don't use that // in the characterisation tests, so it's fine. std::cout << promptForType(promptType) << s << std::endl; diff --git a/src/libcmd/repl.cc b/src/libcmd/repl.cc index f292f06bb81..8170bd579b9 100644 --- a/src/libcmd/repl.cc +++ b/src/libcmd/repl.cc @@ -2,34 +2,34 @@ #include #include -#include "error.hh" -#include "repl-interacter.hh" -#include "repl.hh" - -#include "ansicolor.hh" -#include "shared.hh" -#include "eval.hh" -#include "eval-settings.hh" -#include "attr-path.hh" -#include "signals.hh" -#include "store-api.hh" -#include "log-store.hh" -#include "common-eval-args.hh" -#include "get-drvs.hh" -#include "derivations.hh" -#include "globals.hh" -#include "flake/flake.hh" -#include "flake/lockfile.hh" -#include "users.hh" -#include "editor-for.hh" -#include "finally.hh" -#include "markdown.hh" -#include "local-fs-store.hh" -#include "print.hh" -#include "ref.hh" -#include "value.hh" - -#include "strings.hh" +#include "nix/util/error.hh" +#include "nix/cmd/repl-interacter.hh" +#include "nix/cmd/repl.hh" + +#include "nix/util/ansicolor.hh" +#include "nix/main/shared.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/eval-settings.hh" +#include "nix/expr/attr-path.hh" +#include "nix/util/signals.hh" +#include "nix/store/store-open.hh" +#include "nix/store/log-store.hh" +#include "nix/cmd/common-eval-args.hh" +#include "nix/expr/get-drvs.hh" +#include "nix/store/derivations.hh" +#include "nix/store/globals.hh" +#include "nix/flake/flake.hh" +#include "nix/flake/lockfile.hh" +#include "nix/util/users.hh" +#include "nix/cmd/editor-for.hh" +#include "nix/util/finally.hh" +#include "nix/cmd/markdown.hh" +#include "nix/store/local-fs-store.hh" +#include "nix/expr/print.hh" +#include "nix/util/ref.hh" +#include "nix/expr/value.hh" + +#include "nix/util/strings.hh" namespace nix { @@ -61,11 +61,15 @@ struct NixRepl { size_t debugTraceIndex; + // Arguments passed to :load, saved so they can be reloaded with :reload Strings loadedFiles; + // Arguments passed to :load-flake, saved so they can be reloaded with :reload + Strings loadedFlakes; std::function getValues; const static int envSize = 32768; std::shared_ptr staticEnv; + Value lastLoaded; Env * env; int displ; StringSet varNames; @@ -90,7 +94,9 @@ struct NixRepl void loadFile(const Path & path); void loadFlake(const std::string & flakeRef); void loadFiles(); - void reloadFiles(); + void loadFlakes(); + void reloadFilesAndFlakes(); + void showLastLoaded(); void addAttrsToScope(Value & attrs); void addVarToScope(const Symbol name, Value & v); Expr * parseString(std::string s); @@ -101,6 +107,8 @@ struct NixRepl Value & v, unsigned int maxDepth = std::numeric_limits::max()) { + // Hide the progress bar during printing because it might interfere + auto suspension = logger->suspend(); ::nix::printValue(*state, str, v, PrintOptions { .ansiColors = true, .force = true, @@ -122,11 +130,11 @@ std::string removeWhitespace(std::string s) NixRepl::NixRepl(const LookupPath & lookupPath, nix::ref store, ref state, - std::function getValues, RunNix * runNix = nullptr) + std::function getValues, RunNix * runNix) : AbstractNixRepl(state) , debugTraceIndex(0) , getValues(getValues) - , staticEnv(new StaticEnv(nullptr, state->staticBaseEnv.get())) + , staticEnv(new StaticEnv(nullptr, state->staticBaseEnv)) , runNixPtr{runNix} , interacter(make_unique(getDataDir() + "/repl-history")) { @@ -138,16 +146,13 @@ static std::ostream & showDebugTrace(std::ostream & out, const PosTable & positi out << ANSI_RED "error: " << ANSI_NORMAL; out << dt.hint.str() << "\n"; - // prefer direct pos, but if noPos then try the expr. - auto pos = dt.pos - ? dt.pos - : positions[dt.expr.getPos() ? dt.expr.getPos() : noPos]; + auto pos = dt.getPos(positions); if (pos) { - out << *pos; - if (auto loc = pos->getCodeLines()) { + out << pos; + if (auto loc = pos.getCodeLines()) { out << "\n"; - printCodeLines(out, "", *pos, *loc); + printCodeLines(out, "", pos, *loc); out << "\n"; } } @@ -155,6 +160,8 @@ static std::ostream & showDebugTrace(std::ostream & out, const PosTable & positi return out; } +MakeError(IncompleteReplExpr, ParseError); + static bool isFirstRepl = true; ReplExitStatus NixRepl::mainLoop() @@ -177,18 +184,20 @@ ReplExitStatus NixRepl::mainLoop() while (true) { // Hide the progress bar while waiting for user input, so that it won't interfere. - logger->pause(); - // When continuing input from previous lines, don't print a prompt, just align to the same - // number of chars as the prompt. - if (!interacter->getLine(input, input.empty() ? ReplPromptType::ReplPrompt : ReplPromptType::ContinuationPrompt)) { - // Ctrl-D should exit the debugger. - state->debugStop = false; - logger->cout(""); - // TODO: Should Ctrl-D exit just the current debugger session or - // the entire program? - return ReplExitStatus::QuitAll; + { + auto suspension = logger->suspend(); + // When continuing input from previous lines, don't print a prompt, just align to the same + // number of chars as the prompt. + if (!interacter->getLine(input, input.empty() ? ReplPromptType::ReplPrompt : ReplPromptType::ContinuationPrompt)) { + // Ctrl-D should exit the debugger. + state->debugStop = false; + logger->cout(""); + // TODO: Should Ctrl-D exit just the current debugger session or + // the entire program? + return ReplExitStatus::QuitAll; + } + // `suspension` resumes the logger } - logger->resume(); try { switch (processLine(input)) { case ProcessLineResult::Quit: @@ -200,16 +209,8 @@ ReplExitStatus NixRepl::mainLoop() default: unreachable(); } - } catch (ParseError & e) { - if (e.msg().find("unexpected end of file") != std::string::npos) { - // For parse errors on incomplete input, we continue waiting for the next line of - // input without clearing the input so far. - continue; - } else { - printMsg(lvlError, e.msg()); - } - } catch (EvalError & e) { - printMsg(lvlError, e.msg()); + } catch (IncompleteReplExpr &) { + continue; } catch (Error & e) { printMsg(lvlError, e.msg()); } catch (Interrupted & e) { @@ -243,14 +244,13 @@ StringSet NixRepl::completePrefix(const std::string & prefix) try { auto dir = std::string(cur, 0, slash); auto prefix2 = std::string(cur, slash + 1); - for (auto & entry : std::filesystem::directory_iterator{dir == "" ? "/" : dir}) { + for (auto & entry : DirectoryIterator{dir == "" ? "/" : dir}) { checkInterrupt(); auto name = entry.path().filename().string(); if (name[0] != '.' && hasPrefix(name, prefix2)) completions.insert(prev + entry.path().string()); } } catch (Error &) { - } catch (std::filesystem::filesystem_error &) { } } else if ((dot = cur.rfind('.')) == std::string::npos) { /* This is a variable name; look it up in the current scope. */ @@ -290,7 +290,7 @@ StringSet NixRepl::completePrefix(const std::string & prefix) } catch (BadURL & e) { // Quietly ignore BadURL flake-related errors. } catch (FileNotFound & e) { - // Quietly ignore non-existent file beeing `import`-ed. + // Quietly ignore non-existent file being `import`-ed. } } @@ -374,6 +374,7 @@ ProcessLineResult NixRepl::processLine(std::string line) << " current profile\n" << " :l, :load Load Nix expression and add it to scope\n" << " :lf, :load-flake Load Nix flake and add it to scope\n" + << " :ll, :last-loaded Show most recently loaded variables added to scope\n" << " :p, :print Evaluate and print expression recursively\n" << " Strings are printed directly, without escaping.\n" << " :q, :quit Exit nix-repl\n" @@ -464,9 +465,13 @@ ProcessLineResult NixRepl::processLine(std::string line) loadFlake(arg); } + else if (command == ":ll" || command == ":last-loaded") { + showLastLoaded(); + } + else if (command == ":r" || command == ":reload") { state->resetFileCache(); - reloadFiles(); + reloadFilesAndFlakes(); } else if (command == ":e" || command == ":edit") { @@ -479,7 +484,7 @@ ProcessLineResult NixRepl::processLine(std::string line) auto path = state->coerceToPath(noPos, v, context, "while evaluating the filename to edit"); return {path, 0}; } else if (v.isLambda()) { - auto pos = state->positions[v.payload.lambda.fun->pos]; + auto pos = state->positions[v.lambda().fun->pos]; if (auto path = std::get_if(&pos.origin)) return {*path, pos.line}; else @@ -501,7 +506,7 @@ ProcessLineResult NixRepl::processLine(std::string line) // Reload right after exiting the editor state->resetFileCache(); - reloadFiles(); + reloadFilesAndFlakes(); } else if (command == ":t") { @@ -583,6 +588,7 @@ ProcessLineResult NixRepl::processLine(std::string line) else if (command == ":p" || command == ":print") { Value v; evalString(arg, v); + auto suspension = logger->suspend(); if (v.type() == nString) { std::cout << v.string_view(); } else { @@ -691,6 +697,7 @@ ProcessLineResult NixRepl::processLine(std::string line) } else { Value v; evalString(line, v); + auto suspension = logger->suspend(); printValue(std::cout, v, 1); std::cout << std::endl; } @@ -714,6 +721,9 @@ void NixRepl::loadFlake(const std::string & flakeRefS) if (flakeRefS.empty()) throw Error("cannot use ':load-flake' without a path specified. (Use '.' for the current working directory.)"); + loadedFlakes.remove(flakeRefS); + loadedFlakes.push_back(flakeRefS); + std::filesystem::path cwd; try { cwd = std::filesystem::current_path(); @@ -751,12 +761,23 @@ void NixRepl::initEnv() varNames.emplace(state->symbols[i.first]); } +void NixRepl::showLastLoaded() +{ + RunPager pager; + + for (auto & i : *lastLoaded.attrs()) { + std::string_view name = state->symbols[i.name]; + logger->cout(name); + } +} + -void NixRepl::reloadFiles() +void NixRepl::reloadFilesAndFlakes() { initEnv(); loadFiles(); + loadFlakes(); } @@ -777,6 +798,18 @@ void NixRepl::loadFiles() } +void NixRepl::loadFlakes() +{ + Strings old = loadedFlakes; + loadedFlakes.clear(); + + for (auto & i : old) { + notice("Loading flake '%1%'...", i); + loadFlake(i); + } +} + + void NixRepl::addAttrsToScope(Value & attrs) { state->forceAttrs(attrs, [&]() { return attrs.determinePos(noPos); }, "while evaluating an attribute set to be merged in the global scope"); @@ -791,6 +824,27 @@ void NixRepl::addAttrsToScope(Value & attrs) staticEnv->sort(); staticEnv->deduplicate(); notice("Added %1% variables.", attrs.attrs()->size()); + + lastLoaded = attrs; + + const int max_print = 20; + int counter = 0; + std::ostringstream loaded; + for (auto & i : attrs.attrs()->lexicographicOrder(state->symbols)) { + if (counter >= max_print) + break; + + if (counter > 0) + loaded << ", "; + + printIdentifier(loaded, state->symbols[i->name]); + counter += 1; + } + + notice("%1%", loaded.str()); + + if (attrs.attrs()->size() > max_print) + notice("... and %1% more; view with :ll", attrs.attrs()->size() - max_print); } @@ -815,7 +869,17 @@ Expr * NixRepl::parseString(std::string s) void NixRepl::evalString(std::string s, Value & v) { - Expr * e = parseString(s); + Expr * e; + try { + e = parseString(s); + } catch (ParseError & e) { + if (e.msg().find("unexpected end of file") != std::string::npos) + // For parse errors on incomplete input, we continue waiting for the next line of + // input without clearing the input so far. + throw IncompleteReplExpr(e.msg()); + else + throw; + } e->eval(*state, *env, v); state->forceValue(v, v.determinePos(noPos)); } @@ -836,9 +900,10 @@ std::unique_ptr AbstractNixRepl::create( { return std::make_unique( lookupPath, - openStore(), + std::move(store), state, - getValues + getValues, + runNix ); } @@ -856,7 +921,8 @@ ReplExitStatus AbstractNixRepl::runSimple( lookupPath, openStore(), evalState, - getValues + getValues, + /*runNix=*/nullptr ); repl->initEnv(); diff --git a/src/libexpr-c/build-utils-meson b/src/libexpr-c/build-utils-meson deleted file mode 120000 index 5fff21bab55..00000000000 --- a/src/libexpr-c/build-utils-meson +++ /dev/null @@ -1 +0,0 @@ -../../build-utils-meson \ No newline at end of file diff --git a/src/libexpr-c/meson.build b/src/libexpr-c/meson.build index 5bcca29e0b8..ed4582e4084 100644 --- a/src/libexpr-c/meson.build +++ b/src/libexpr-c/meson.build @@ -4,8 +4,6 @@ project('nix-expr-c', 'cpp', 'cpp_std=c++2a', # TODO(Qyriad): increase the warning level 'warning_level=1', - 'debug=true', - 'optimization=2', 'errorlogs=true', # Please print logs for tests that fail ], meson_version : '>= 1.1', @@ -14,9 +12,7 @@ project('nix-expr-c', 'cpp', cxx = meson.get_compiler('cpp') -subdir('build-utils-meson/deps-lists') - -configdata = configuration_data() +subdir('nix-meson-build-support/deps-lists') deps_private_maybe_subproject = [ dependency('nix-util'), @@ -27,33 +23,9 @@ deps_public_maybe_subproject = [ dependency('nix-util-c'), dependency('nix-store-c'), ] -subdir('build-utils-meson/subprojects') - -# TODO rename, because it will conflict with downstream projects -configdata.set_quoted('PACKAGE_VERSION', meson.project_version()) - -config_h = configure_file( - configuration : configdata, - output : 'config-expr.h', -) - -add_project_arguments( - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. +subdir('nix-meson-build-support/subprojects') - # From C++ libraries, only for internals - '-include', 'config-util.hh', - '-include', 'config-store.hh', - '-include', 'config-expr.hh', - - # From C libraries, for our public, installed headers too - '-include', 'config-util.h', - '-include', 'config-store.h', - '-include', 'config-expr.h', - language : 'cpp', -) - -subdir('build-utils-meson/common') +subdir('nix-meson-build-support/common') sources = files( 'nix_api_expr.cc', @@ -63,17 +35,15 @@ sources = files( include_dirs = [include_directories('.')] -headers = [config_h] + files( +headers = files( 'nix_api_expr.h', + 'nix_api_expr_internal.h', 'nix_api_external.h', 'nix_api_value.h', ) -# TODO move this header to libexpr, maybe don't use it in tests? -headers += files('nix_api_expr_internal.h') - -subdir('build-utils-meson/export-all-symbols') -subdir('build-utils-meson/windows-version') +subdir('nix-meson-build-support/export-all-symbols') +subdir('nix-meson-build-support/windows-version') this_library = library( 'nixexprc', @@ -85,8 +55,8 @@ this_library = library( install : true, ) -install_headers(headers, subdir : 'nix', preserve_path : true) +install_headers(headers, preserve_path : true) libraries_private = [] -subdir('build-utils-meson/export') +subdir('nix-meson-build-support/export') diff --git a/src/libexpr-c/nix-meson-build-support b/src/libexpr-c/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/libexpr-c/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/libexpr-c/nix_api_expr.cc b/src/libexpr-c/nix_api_expr.cc index a024248cdd0..efaebf0e742 100644 --- a/src/libexpr-c/nix_api_expr.cc +++ b/src/libexpr-c/nix_api_expr.cc @@ -2,11 +2,11 @@ #include #include -#include "eval.hh" -#include "eval-gc.hh" -#include "globals.hh" -#include "eval-settings.hh" -#include "ref.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/eval-gc.hh" +#include "nix/store/globals.hh" +#include "nix/expr/eval-settings.hh" +#include "nix/util/ref.hh" #include "nix_api_expr.h" #include "nix_api_expr_internal.h" @@ -15,7 +15,7 @@ #include "nix_api_util.h" #include "nix_api_util_internal.h" -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC # include #endif @@ -199,7 +199,9 @@ EvalState * nix_state_create(nix_c_context * context, const char ** lookupPath_c != NIX_OK) return nullptr; - return nix_eval_state_build(context, builder); + auto *state = nix_eval_state_build(context, builder); + nix_eval_state_builder_free(builder); + return state; } void nix_state_free(EvalState * state) @@ -207,7 +209,7 @@ void nix_state_free(EvalState * state) delete state; } -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC std::unordered_map< const void *, unsigned int, @@ -283,7 +285,7 @@ nix_err nix_value_decref(nix_c_context * context, nix_value *x) void nix_gc_register_finalizer(void * obj, void * cd, void (*finalizer)(void * obj, void * cd)) { -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC GC_REGISTER_FINALIZER(obj, finalizer, cd, 0, 0); #endif } diff --git a/src/libexpr-c/nix_api_expr.h b/src/libexpr-c/nix_api_expr.h index f8d1814521b..2be7399551e 100644 --- a/src/libexpr-c/nix_api_expr.h +++ b/src/libexpr-c/nix_api_expr.h @@ -286,6 +286,11 @@ nix_err nix_gc_incref(nix_c_context * context, const void * object); /** * @brief Decrement the garbage collector reference counter for the given object * + * We also provide typed `nix_*_decref` functions, which are + * - safer to use + * - easier to integrate when deriving bindings + * - allow more flexibility + * * @param[out] context Optional, stores error information * @param[in] object The object to stop referencing */ diff --git a/src/libexpr-c/nix_api_expr_internal.h b/src/libexpr-c/nix_api_expr_internal.h index f596640115f..a26595cec5d 100644 --- a/src/libexpr-c/nix_api_expr_internal.h +++ b/src/libexpr-c/nix_api_expr_internal.h @@ -1,12 +1,12 @@ #ifndef NIX_API_EXPR_INTERNAL_H #define NIX_API_EXPR_INTERNAL_H -#include "fetch-settings.hh" -#include "eval.hh" -#include "eval-settings.hh" -#include "attr-set.hh" +#include "nix/fetchers/fetch-settings.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/eval-settings.hh" +#include "nix/expr/attr-set.hh" #include "nix_api_value.h" -#include "search-path.hh" +#include "nix/expr/search-path.hh" struct nix_eval_state_builder { diff --git a/src/libexpr-c/nix_api_external.cc b/src/libexpr-c/nix_api_external.cc index d673bcb0b30..04d2e52b564 100644 --- a/src/libexpr-c/nix_api_external.cc +++ b/src/libexpr-c/nix_api_external.cc @@ -1,8 +1,8 @@ -#include "attr-set.hh" -#include "config.hh" -#include "eval.hh" -#include "globals.hh" -#include "value.hh" +#include "nix/expr/attr-set.hh" +#include "nix/util/configuration.hh" +#include "nix/expr/eval.hh" +#include "nix/store/globals.hh" +#include "nix/expr/value.hh" #include "nix_api_expr.h" #include "nix_api_expr_internal.h" @@ -10,7 +10,7 @@ #include "nix_api_util.h" #include "nix_api_util_internal.h" #include "nix_api_value.h" -#include "value/context.hh" +#include "nix/expr/value/context.hh" #include @@ -168,7 +168,7 @@ ExternalValue * nix_create_external_value(nix_c_context * context, NixCExternalV context->last_err_code = NIX_OK; try { auto ret = new -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC (GC) #endif NixCExternalValue(*desc, v); diff --git a/src/libexpr-c/nix_api_external.h b/src/libexpr-c/nix_api_external.h index 6c524b9755d..f4a32728100 100644 --- a/src/libexpr-c/nix_api_external.h +++ b/src/libexpr-c/nix_api_external.h @@ -12,9 +12,10 @@ #include "nix_api_expr.h" #include "nix_api_util.h" #include "nix_api_value.h" -#include "stdbool.h" -#include "stddef.h" -#include "stdint.h" + +#include +#include +#include #ifdef __cplusplus extern "C" { diff --git a/src/libexpr-c/nix_api_value.cc b/src/libexpr-c/nix_api_value.cc index bae078d312f..fb90e2872e6 100644 --- a/src/libexpr-c/nix_api_value.cc +++ b/src/libexpr-c/nix_api_value.cc @@ -1,10 +1,10 @@ -#include "attr-set.hh" -#include "config.hh" -#include "eval.hh" -#include "globals.hh" -#include "path.hh" -#include "primops.hh" -#include "value.hh" +#include "nix/expr/attr-set.hh" +#include "nix/util/configuration.hh" +#include "nix/expr/eval.hh" +#include "nix/store/globals.hh" +#include "nix/store/path.hh" +#include "nix/expr/primops.hh" +#include "nix/expr/value.hh" #include "nix_api_expr.h" #include "nix_api_expr_internal.h" @@ -12,7 +12,7 @@ #include "nix_api_util_internal.h" #include "nix_api_store_internal.h" #include "nix_api_value.h" -#include "value/context.hh" +#include "nix/expr/value/context.hh" // Internal helper functions to check [in] and [out] `Value *` parameters static const nix::Value & check_value_not_null(const nix_value * value) @@ -125,7 +125,7 @@ PrimOp * nix_alloc_primop( try { using namespace std::placeholders; auto p = new -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC (GC) #endif nix::PrimOp{ @@ -252,7 +252,7 @@ const char * nix_get_path_string(nix_c_context * context, const nix_value * valu // We could use v.path().to_string().c_str(), but I'm concerned this // crashes. Looks like .path() allocates a CanonPath with a copy of the // string, then it gets the underlying data from that. - return v.payload.path.path; + return v.pathStr(); } NIXC_CATCH_ERRS_NULL } @@ -324,7 +324,7 @@ nix_value * nix_get_list_byidx(nix_c_context * context, const nix_value * value, try { auto & v = check_value_in(value); assert(v.type() == nix::nList); - auto * p = v.listElems()[ix]; + auto * p = v.listView()[ix]; nix_gc_incref(nullptr, p); if (p != nullptr) state->state.forceValue(*p, nix::noPos); @@ -497,7 +497,7 @@ ListBuilder * nix_make_list_builder(nix_c_context * context, EvalState * state, try { auto builder = state->state.buildList(capacity); return new -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC (NoGC) #endif ListBuilder{std::move(builder)}; @@ -519,7 +519,7 @@ nix_list_builder_insert(nix_c_context * context, ListBuilder * list_builder, uns void nix_list_builder_free(ListBuilder * list_builder) { -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC GC_FREE(list_builder); #else delete list_builder; @@ -578,7 +578,7 @@ BindingsBuilder * nix_make_bindings_builder(nix_c_context * context, EvalState * try { auto bb = state->state.buildBindings(capacity); return new -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC (NoGC) #endif BindingsBuilder{std::move(bb)}; @@ -600,7 +600,7 @@ nix_err nix_bindings_builder_insert(nix_c_context * context, BindingsBuilder * b void nix_bindings_builder_free(BindingsBuilder * bb) { -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC GC_FREE((nix::BindingsBuilder *) bb); #else delete (nix::BindingsBuilder *) bb; @@ -613,12 +613,8 @@ nix_realised_string * nix_string_realise(nix_c_context * context, EvalState * st context->last_err_code = NIX_OK; try { auto & v = check_value_in(value); - nix::NixStringContext stringContext; - auto rawStr = state->state.coerceToString(nix::noPos, v, stringContext, "while realising a string").toOwned(); nix::StorePathSet storePaths; - auto rewrites = state->state.realiseContext(stringContext, &storePaths); - - auto s = nix::rewriteStrings(rawStr, rewrites); + auto s = state->state.realiseString(v, &storePaths, isIFD); // Convert to the C API StorePath type and convert to vector for index-based access std::vector vec; diff --git a/src/libexpr-c/nix_api_value.h b/src/libexpr-c/nix_api_value.h index 711b0adbc82..7cd6ad18087 100644 --- a/src/libexpr-c/nix_api_value.h +++ b/src/libexpr-c/nix_api_value.h @@ -10,9 +10,10 @@ #include "nix_api_util.h" #include "nix_api_store.h" -#include "stdbool.h" -#include "stddef.h" -#include "stdint.h" + +#include +#include +#include #ifdef __cplusplus extern "C" { diff --git a/src/libexpr-c/package.nix b/src/libexpr-c/package.nix index df49a8bdc2f..694fbc1fe78 100644 --- a/src/libexpr-c/package.nix +++ b/src/libexpr-c/package.nix @@ -1,13 +1,13 @@ -{ lib -, stdenv -, mkMesonLibrary +{ + lib, + mkMesonLibrary, -, nix-store-c -, nix-expr + nix-store-c, + nix-expr, -# Configuration Options + # Configuration Options -, version + version, }: let @@ -20,8 +20,8 @@ mkMesonLibrary (finalAttrs: { workDir = ./.; fileset = fileset.unions [ - ../../build-utils-meson - ./build-utils-meson + ../../nix-meson-build-support + ./nix-meson-build-support ../../.version ./.version ./meson.build @@ -36,21 +36,9 @@ mkMesonLibrary (finalAttrs: { nix-expr ]; - preConfigure = - # "Inline" .version so it's not a symlink, and includes the suffix. - # Do the meson utils, without modification. - '' - chmod u+w ./.version - echo ${version} > ../../.version - ''; - mesonFlags = [ ]; - env = lib.optionalAttrs (stdenv.isLinux && !(stdenv.hostPlatform.isStatic && stdenv.system == "aarch64-linux")) { - LDFLAGS = "-fuse-ld=gold"; - }; - meta = { platforms = lib.platforms.unix ++ lib.platforms.windows; }; diff --git a/src/libexpr-test-support/build-utils-meson b/src/libexpr-test-support/build-utils-meson deleted file mode 120000 index 5fff21bab55..00000000000 --- a/src/libexpr-test-support/build-utils-meson +++ /dev/null @@ -1 +0,0 @@ -../../build-utils-meson \ No newline at end of file diff --git a/src/libexpr-test-support/tests/libexpr.hh b/src/libexpr-test-support/include/nix/expr/tests/libexpr.hh similarity index 93% rename from src/libexpr-test-support/tests/libexpr.hh rename to src/libexpr-test-support/include/nix/expr/tests/libexpr.hh index 095ea1d0e4b..48c96ae2cdf 100644 --- a/src/libexpr-test-support/tests/libexpr.hh +++ b/src/libexpr-test-support/include/nix/expr/tests/libexpr.hh @@ -4,16 +4,16 @@ #include #include -#include "fetch-settings.hh" -#include "value.hh" -#include "nixexpr.hh" -#include "nixexpr.hh" -#include "eval.hh" -#include "eval-gc.hh" -#include "eval-inline.hh" -#include "eval-settings.hh" - -#include "tests/libstore.hh" +#include "nix/fetchers/fetch-settings.hh" +#include "nix/expr/value.hh" +#include "nix/expr/nixexpr.hh" +#include "nix/expr/nixexpr.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/eval-gc.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/expr/eval-settings.hh" + +#include "nix/store/tests/libstore.hh" namespace nix { class LibExprTest : public LibStoreTest { diff --git a/src/libexpr-test-support/include/nix/expr/tests/meson.build b/src/libexpr-test-support/include/nix/expr/tests/meson.build new file mode 100644 index 00000000000..84ec401abad --- /dev/null +++ b/src/libexpr-test-support/include/nix/expr/tests/meson.build @@ -0,0 +1,10 @@ +# Public headers directory + +include_dirs = [ include_directories('../../..') ] + +headers = files( + 'libexpr.hh', + 'nix_api_expr.hh', + 'value/context.hh', + # hack for trailing newline +) diff --git a/src/libexpr-test-support/tests/nix_api_expr.hh b/src/libexpr-test-support/include/nix/expr/tests/nix_api_expr.hh similarity index 92% rename from src/libexpr-test-support/tests/nix_api_expr.hh rename to src/libexpr-test-support/include/nix/expr/tests/nix_api_expr.hh index 6ddca0d14d4..3e5aec31369 100644 --- a/src/libexpr-test-support/tests/nix_api_expr.hh +++ b/src/libexpr-test-support/include/nix/expr/tests/nix_api_expr.hh @@ -2,7 +2,7 @@ ///@file #include "nix_api_expr.h" #include "nix_api_value.h" -#include "tests/nix_api_store.hh" +#include "nix/store/tests/nix_api_store.hh" #include diff --git a/src/libexpr-test-support/tests/value/context.hh b/src/libexpr-test-support/include/nix/expr/tests/value/context.hh similarity index 93% rename from src/libexpr-test-support/tests/value/context.hh rename to src/libexpr-test-support/include/nix/expr/tests/value/context.hh index 8c68c78bbd1..a6a851d3ac7 100644 --- a/src/libexpr-test-support/tests/value/context.hh +++ b/src/libexpr-test-support/include/nix/expr/tests/value/context.hh @@ -3,7 +3,7 @@ #include -#include "value/context.hh" +#include "nix/expr/value/context.hh" namespace rc { using namespace nix; diff --git a/src/libexpr-test-support/meson.build b/src/libexpr-test-support/meson.build index 33d9e17a6a8..b97f94362fd 100644 --- a/src/libexpr-test-support/meson.build +++ b/src/libexpr-test-support/meson.build @@ -4,8 +4,6 @@ project('nix-expr-test-support', 'cpp', 'cpp_std=c++2a', # TODO(Qyriad): increase the warning level 'warning_level=1', - 'debug=true', - 'optimization=2', 'errorlogs=true', # Please print logs for tests that fail ], meson_version : '>= 1.1', @@ -14,7 +12,7 @@ project('nix-expr-test-support', 'cpp', cxx = meson.get_compiler('cpp') -subdir('build-utils-meson/deps-lists') +subdir('nix-meson-build-support/deps-lists') deps_private_maybe_subproject = [ ] @@ -26,36 +24,21 @@ deps_public_maybe_subproject = [ dependency('nix-expr'), dependency('nix-expr-c'), ] -subdir('build-utils-meson/subprojects') +subdir('nix-meson-build-support/subprojects') rapidcheck = dependency('rapidcheck') deps_public += rapidcheck -add_project_arguments( - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. - '-include', 'config-util.hh', - '-include', 'config-store.hh', - '-include', 'config-expr.hh', - language : 'cpp', -) - -subdir('build-utils-meson/common') +subdir('nix-meson-build-support/common') sources = files( 'tests/value/context.cc', ) -include_dirs = [include_directories('.')] - -headers = files( - 'tests/libexpr.hh', - 'tests/nix_api_expr.hh', - 'tests/value/context.hh', -) +subdir('include/nix/expr/tests') -subdir('build-utils-meson/export-all-symbols') -subdir('build-utils-meson/windows-version') +subdir('nix-meson-build-support/export-all-symbols') +subdir('nix-meson-build-support/windows-version') this_library = library( 'nix-expr-test-support', @@ -69,8 +52,8 @@ this_library = library( install : true, ) -install_headers(headers, subdir : 'nix', preserve_path : true) +install_headers(headers, subdir : 'nix/expr/tests', preserve_path : true) libraries_private = [] -subdir('build-utils-meson/export') +subdir('nix-meson-build-support/export') diff --git a/src/libexpr-test-support/nix-meson-build-support b/src/libexpr-test-support/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/libexpr-test-support/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/libexpr-test-support/package.nix b/src/libexpr-test-support/package.nix index 7e92d145f78..5cb4adaa8c4 100644 --- a/src/libexpr-test-support/package.nix +++ b/src/libexpr-test-support/package.nix @@ -1,16 +1,16 @@ -{ lib -, stdenv -, mkMesonLibrary +{ + lib, + mkMesonLibrary, -, nix-store-test-support -, nix-expr -, nix-expr-c + nix-store-test-support, + nix-expr, + nix-expr-c, -, rapidcheck + rapidcheck, -# Configuration Options + # Configuration Options -, version + version, }: let @@ -23,12 +23,13 @@ mkMesonLibrary (finalAttrs: { workDir = ./.; fileset = fileset.unions [ - ../../build-utils-meson - ./build-utils-meson + ../../nix-meson-build-support + ./nix-meson-build-support ../../.version ./.version ./meson.build # ./meson.options + ./include/nix/expr/tests/meson.build (fileset.fileFilter (file: file.hasExt "cc") ./.) (fileset.fileFilter (file: file.hasExt "hh") ./.) ]; @@ -40,21 +41,9 @@ mkMesonLibrary (finalAttrs: { rapidcheck ]; - preConfigure = - # "Inline" .version so it's not a symlink, and includes the suffix. - # Do the meson utils, without modification. - '' - chmod u+w ./.version - echo ${version} > ../../.version - ''; - mesonFlags = [ ]; - env = lib.optionalAttrs (stdenv.isLinux && !(stdenv.hostPlatform.isStatic && stdenv.system == "aarch64-linux")) { - LDFLAGS = "-fuse-ld=gold"; - }; - meta = { platforms = lib.platforms.unix ++ lib.platforms.windows; }; diff --git a/src/libexpr-test-support/tests/value/context.cc b/src/libexpr-test-support/tests/value/context.cc index 8658bdaef16..51ff1b2ae61 100644 --- a/src/libexpr-test-support/tests/value/context.cc +++ b/src/libexpr-test-support/tests/value/context.cc @@ -1,30 +1,39 @@ #include -#include "tests/path.hh" -#include "tests/value/context.hh" +#include "nix/store/tests/path.hh" +#include "nix/expr/tests/value/context.hh" namespace rc { using namespace nix; Gen Arbitrary::arbitrary() { - return gen::just(NixStringContextElem::DrvDeep { - .drvPath = *gen::arbitrary(), + return gen::map(gen::arbitrary(), [](StorePath drvPath) { + return NixStringContextElem::DrvDeep{ + .drvPath = drvPath, + }; }); } Gen Arbitrary::arbitrary() { - switch (*gen::inRange(0, std::variant_size_v)) { - case 0: - return gen::just(*gen::arbitrary()); - case 1: - return gen::just(*gen::arbitrary()); - case 2: - return gen::just(*gen::arbitrary()); - default: - assert(false); - } + return gen::mapcat( + gen::inRange(0, std::variant_size_v), + [](uint8_t n) -> Gen { + switch (n) { + case 0: + return gen::map( + gen::arbitrary(), [](NixStringContextElem a) { return a; }); + case 1: + return gen::map( + gen::arbitrary(), [](NixStringContextElem a) { return a; }); + case 2: + return gen::map( + gen::arbitrary(), [](NixStringContextElem a) { return a; }); + default: + assert(false); + } + }); } } diff --git a/src/libexpr-tests/build-utils-meson b/src/libexpr-tests/build-utils-meson deleted file mode 120000 index 5fff21bab55..00000000000 --- a/src/libexpr-tests/build-utils-meson +++ /dev/null @@ -1 +0,0 @@ -../../build-utils-meson \ No newline at end of file diff --git a/src/libexpr-tests/derived-path.cc b/src/libexpr-tests/derived-path.cc index d5fc6f20155..9cc5d53714b 100644 --- a/src/libexpr-tests/derived-path.cc +++ b/src/libexpr-tests/derived-path.cc @@ -2,8 +2,8 @@ #include #include -#include "tests/derived-path.hh" -#include "tests/libexpr.hh" +#include "nix/store/tests/derived-path.hh" +#include "nix/expr/tests/libexpr.hh" namespace nix { @@ -44,11 +44,11 @@ RC_GTEST_FIXTURE_PROP( * to worry about race conditions if the tests run concurrently. */ ExperimentalFeatureSettings mockXpSettings; - mockXpSettings.set("experimental-features", "ca-derivations"); + mockXpSettings.set("experimental-features", "ca-derivations dynamic-derivations"); auto * v = state.allocValue(); state.mkOutputString(*v, b, std::nullopt, mockXpSettings); - auto [d, _] = state.coerceToSingleDerivedPathUnchecked(noPos, *v, ""); + auto [d, _] = state.coerceToSingleDerivedPathUnchecked(noPos, *v, "", mockXpSettings); RC_ASSERT(SingleDerivedPath { b } == d); } @@ -57,9 +57,12 @@ RC_GTEST_FIXTURE_PROP( prop_derived_path_built_out_path_round_trip, (const SingleDerivedPath::Built & b, const StorePath & outPath)) { + ExperimentalFeatureSettings mockXpSettings; + mockXpSettings.set("experimental-features", "dynamic-derivations"); + auto * v = state.allocValue(); - state.mkOutputString(*v, b, outPath); - auto [d, _] = state.coerceToSingleDerivedPathUnchecked(noPos, *v, ""); + state.mkOutputString(*v, b, outPath, mockXpSettings); + auto [d, _] = state.coerceToSingleDerivedPathUnchecked(noPos, *v, "", mockXpSettings); RC_ASSERT(SingleDerivedPath { b } == d); } diff --git a/src/libexpr-tests/error_traces.cc b/src/libexpr-tests/error_traces.cc index be379a90993..32e49efe6c9 100644 --- a/src/libexpr-tests/error_traces.cc +++ b/src/libexpr-tests/error_traces.cc @@ -1,7 +1,7 @@ #include #include -#include "tests/libexpr.hh" +#include "nix/expr/tests/libexpr.hh" namespace nix { @@ -33,7 +33,7 @@ namespace nix { ASSERT_EQ(PrintToString(e.info().msg), PrintToString(HintFmt("puppy"))); auto trace = e.info().traces.rbegin(); - ASSERT_EQ(e.info().traces.size(), 2); + ASSERT_EQ(e.info().traces.size(), 2u); ASSERT_EQ(PrintToString(trace->hint), PrintToString(HintFmt("doggy"))); trace++; @@ -54,8 +54,8 @@ namespace nix { } catch (Error & e2) { e.addTrace(state.positions[noPos], "beans2"); //e2.addTrace(state.positions[noPos], "Something", ""); - ASSERT_TRUE(e.info().traces.size() == 2); - ASSERT_TRUE(e2.info().traces.size() == 0); + ASSERT_TRUE(e.info().traces.size() == 2u); + ASSERT_TRUE(e2.info().traces.size() == 0u); ASSERT_FALSE(&e.info() == &e2.info()); } } @@ -71,7 +71,7 @@ namespace nix { } catch (BaseError & e) { \ ASSERT_EQ(PrintToString(e.info().msg), \ PrintToString(message)); \ - ASSERT_EQ(e.info().traces.size(), 1) << "while testing " args << std::endl << e.what(); \ + ASSERT_EQ(e.info().traces.size(), 1u) << "while testing " args << std::endl << e.what(); \ auto trace = e.info().traces.rbegin(); \ ASSERT_EQ(PrintToString(trace->hint), \ PrintToString(HintFmt("while calling the '%s' builtin", name))); \ @@ -90,7 +90,7 @@ namespace nix { } catch (BaseError & e) { \ ASSERT_EQ(PrintToString(e.info().msg), \ PrintToString(message)); \ - ASSERT_EQ(e.info().traces.size(), 2) << "while testing " args << std::endl << e.what(); \ + ASSERT_EQ(e.info().traces.size(), 2u) << "while testing " args << std::endl << e.what(); \ auto trace = e.info().traces.rbegin(); \ ASSERT_EQ(PrintToString(trace->hint), \ PrintToString(context)); \ @@ -112,7 +112,7 @@ namespace nix { } catch (BaseError & e) { \ ASSERT_EQ(PrintToString(e.info().msg), \ PrintToString(message)); \ - ASSERT_EQ(e.info().traces.size(), 3) << "while testing " args << std::endl << e.what(); \ + ASSERT_EQ(e.info().traces.size(), 3u) << "while testing " args << std::endl << e.what(); \ auto trace = e.info().traces.rbegin(); \ ASSERT_EQ(PrintToString(trace->hint), \ PrintToString(context1)); \ @@ -137,7 +137,7 @@ namespace nix { } catch (BaseError & e) { \ ASSERT_EQ(PrintToString(e.info().msg), \ PrintToString(message)); \ - ASSERT_EQ(e.info().traces.size(), 4) << "while testing " args << std::endl << e.what(); \ + ASSERT_EQ(e.info().traces.size(), 4u) << "while testing " args << std::endl << e.what(); \ auto trace = e.info().traces.rbegin(); \ ASSERT_EQ(PrintToString(trace->hint), \ PrintToString(context1)); \ @@ -458,7 +458,7 @@ namespace nix { HintFmt("expected a function but found %s: %s", "a list", Uncolored("[ ]")), HintFmt("while evaluating the first argument passed to builtins.filterSource")); - // Usupported by store "dummy" + // Unsupported by store "dummy" // ASSERT_TRACE2("filterSource (_: 1) ./.", // TypeError, @@ -636,7 +636,7 @@ namespace nix { HintFmt("expected a set but found %s: %s", "a list", Uncolored("[ ]")), HintFmt("while evaluating the second argument passed to builtins.mapAttrs")); - // XXX: defered + // XXX: deferred // ASSERT_TRACE2("mapAttrs \"\" { foo.bar = 1; }", // TypeError, // HintFmt("attempt to call something which is not a function but %s", "a string"), @@ -666,9 +666,9 @@ namespace nix { HintFmt("expected a set but found %s: %s", "an integer", Uncolored(ANSI_CYAN "1" ANSI_NORMAL)), HintFmt("while evaluating a value of the list passed as second argument to builtins.zipAttrsWith")); - // XXX: How to properly tell that the fucntion takes two arguments ? + // XXX: How to properly tell that the function takes two arguments ? // The same question also applies to sort, and maybe others. - // Due to lazyness, we only create a thunk, and it fails later on. + // Due to laziness, we only create a thunk, and it fails later on. // ASSERT_TRACE2("zipAttrsWith (_: 1) [ { foo = 1; } ]", // TypeError, // HintFmt("attempt to call something which is not a function but %s", "an integer"), @@ -691,15 +691,15 @@ namespace nix { ASSERT_TRACE2("elemAt \"foo\" (-1)", TypeError, HintFmt("expected a list but found %s: %s", "a string", Uncolored(ANSI_MAGENTA "\"foo\"" ANSI_NORMAL)), - HintFmt("while evaluating the first argument passed to builtins.elemAt")); + HintFmt("while evaluating the first argument passed to 'builtins.elemAt'")); ASSERT_TRACE1("elemAt [] (-1)", Error, - HintFmt("list index %d is out of bounds", -1)); + HintFmt("'builtins.elemAt' called with index %d on a list of size %d", -1, 0)); ASSERT_TRACE1("elemAt [\"foo\"] 3", Error, - HintFmt("list index %d is out of bounds", 3)); + HintFmt("'builtins.elemAt' called with index %d on a list of size %d", 3, 1)); } @@ -708,11 +708,11 @@ namespace nix { ASSERT_TRACE2("head 1", TypeError, HintFmt("expected a list but found %s: %s", "an integer", Uncolored(ANSI_CYAN "1" ANSI_NORMAL)), - HintFmt("while evaluating the first argument passed to builtins.elemAt")); + HintFmt("while evaluating the first argument passed to 'builtins.head'")); ASSERT_TRACE1("head []", Error, - HintFmt("list index %d is out of bounds", 0)); + HintFmt("'builtins.head' called on an empty list")); } @@ -721,11 +721,11 @@ namespace nix { ASSERT_TRACE2("tail 1", TypeError, HintFmt("expected a list but found %s: %s", "an integer", Uncolored(ANSI_CYAN "1" ANSI_NORMAL)), - HintFmt("while evaluating the first argument passed to builtins.tail")); + HintFmt("while evaluating the first argument passed to 'builtins.tail'")); ASSERT_TRACE1("tail []", Error, - HintFmt("'tail' called on an empty list")); + HintFmt("'builtins.tail' called on an empty list")); } @@ -877,7 +877,7 @@ namespace nix { HintFmt("expected a function but found %s: %s", "an integer", Uncolored(ANSI_CYAN "1" ANSI_NORMAL)), HintFmt("while evaluating the first argument passed to builtins.genList")); - // XXX: defered + // XXX: deferred // ASSERT_TRACE2("genList (x: x + \"foo\") 2 #TODO", // TypeError, // HintFmt("cannot add %s to an integer", "a string"), @@ -1152,7 +1152,7 @@ namespace nix { ASSERT_TRACE1("hashString \"foo\" \"content\"", UsageError, - HintFmt("unknown hash algorithm '%s', expect 'md5', 'sha1', 'sha256', or 'sha512'", "foo")); + HintFmt("unknown hash algorithm '%s', expect 'blake3', 'md5', 'sha1', 'sha256', or 'sha512'", "foo")); ASSERT_TRACE2("hashString \"sha256\" {}", TypeError, diff --git a/src/libexpr-tests/eval.cc b/src/libexpr-tests/eval.cc index 61f6be0db6f..e9664dc5892 100644 --- a/src/libexpr-tests/eval.cc +++ b/src/libexpr-tests/eval.cc @@ -1,8 +1,8 @@ #include #include -#include "eval.hh" -#include "tests/libexpr.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/tests/libexpr.hh" namespace nix { diff --git a/src/libexpr-tests/json.cc b/src/libexpr-tests/json.cc index f4cc118d664..11f31d05851 100644 --- a/src/libexpr-tests/json.cc +++ b/src/libexpr-tests/json.cc @@ -1,5 +1,5 @@ -#include "tests/libexpr.hh" -#include "value-to-json.hh" +#include "nix/expr/tests/libexpr.hh" +#include "nix/expr/value-to-json.hh" namespace nix { // Testing the conversion to JSON diff --git a/src/libexpr-tests/main.cc b/src/libexpr-tests/main.cc index e3412d9ef9a..52cca53c407 100644 --- a/src/libexpr-tests/main.cc +++ b/src/libexpr-tests/main.cc @@ -1,7 +1,7 @@ #include #include -#include "globals.hh" -#include "logging.hh" +#include "nix/store/globals.hh" +#include "nix/util/logging.hh" using namespace nix; @@ -14,7 +14,7 @@ int main (int argc, char **argv) { // Disable build hook. We won't be testing remote builds in these unit tests. If we do, fix the above build hook. settings.buildHook = {}; - #if __linux__ // should match the conditional around sandboxBuildDir declaration. + #ifdef __linux__ // should match the conditional around sandboxBuildDir declaration. // When building and testing nix within the host's Nix sandbox, our store dir will be located in the host's sandboxBuildDir, e.g.: // Host @@ -27,7 +27,7 @@ int main (int argc, char **argv) { settings.sandboxBuildDir = "/test-build-dir-instead-of-usual-build-dir"; #endif - #if __APPLE__ + #ifdef __APPLE__ // Avoid this error, when already running in a sandbox: // sandbox-exec: sandbox_apply: Operation not permitted settings.sandboxMode = smDisabled; diff --git a/src/libexpr-tests/meson.build b/src/libexpr-tests/meson.build index b50c18c9c8f..35ae8a9d068 100644 --- a/src/libexpr-tests/meson.build +++ b/src/libexpr-tests/meson.build @@ -4,8 +4,6 @@ project('nix-expr-tests', 'cpp', 'cpp_std=c++2a', # TODO(Qyriad): increase the warning level 'warning_level=1', - 'debug=true', - 'optimization=2', 'errorlogs=true', # Please print logs for tests that fail ], meson_version : '>= 1.1', @@ -14,7 +12,7 @@ project('nix-expr-tests', 'cpp', cxx = meson.get_compiler('cpp') -subdir('build-utils-meson/deps-lists') +subdir('nix-meson-build-support/deps-lists') deps_private_maybe_subproject = [ dependency('nix-expr'), @@ -23,10 +21,10 @@ deps_private_maybe_subproject = [ ] deps_public_maybe_subproject = [ ] -subdir('build-utils-meson/subprojects') +subdir('nix-meson-build-support/subprojects') -subdir('build-utils-meson/export-all-symbols') -subdir('build-utils-meson/windows-version') +subdir('nix-meson-build-support/export-all-symbols') +subdir('nix-meson-build-support/windows-version') rapidcheck = dependency('rapidcheck') deps_private += rapidcheck @@ -34,22 +32,18 @@ deps_private += rapidcheck gtest = dependency('gtest') deps_private += gtest -gtest = dependency('gmock') -deps_private += gtest +gmock = dependency('gmock') +deps_private += gmock + +configdata = configuration_data() +configdata.set_quoted('PACKAGE_VERSION', meson.project_version()) -add_project_arguments( - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. - '-include', 'config-util.hh', - '-include', 'config-store.hh', - '-include', 'config-expr.hh', - '-include', 'config-util.h', - '-include', 'config-store.h', - '-include', 'config-expr.h', - language : 'cpp', +config_priv_h = configure_file( + configuration : configdata, + output : 'expr-tests-config.hh', ) -subdir('build-utils-meson/common') +subdir('nix-meson-build-support/common') sources = files( 'derived-path.cc', @@ -74,6 +68,7 @@ include_dirs = [include_directories('.')] this_exe = executable( meson.project_name(), sources, + config_priv_h, dependencies : deps_private_subproject + deps_private + deps_other, include_directories : include_dirs, # TODO: -lrapidcheck, see ../libutil-support/build.meson diff --git a/src/libexpr-tests/nix-meson-build-support b/src/libexpr-tests/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/libexpr-tests/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/libexpr-tests/nix_api_expr.cc b/src/libexpr-tests/nix_api_expr.cc index 5ed78d2fcd9..f3b6fed0ea1 100644 --- a/src/libexpr-tests/nix_api_expr.cc +++ b/src/libexpr-tests/nix_api_expr.cc @@ -5,13 +5,15 @@ #include "nix_api_expr.h" #include "nix_api_value.h" -#include "tests/nix_api_expr.hh" -#include "tests/string_callback.hh" -#include "file-system.hh" +#include "nix/expr/tests/nix_api_expr.hh" +#include "nix/util/tests/string_callback.hh" +#include "nix/util/file-system.hh" #include #include +#include "expr-tests-config.hh" + namespace nixC { TEST_F(nix_api_store_test, nix_eval_state_lookup_path) @@ -172,7 +174,7 @@ TEST_F(nix_api_expr_test, nix_expr_realise_context_bad_build) TEST_F(nix_api_expr_test, nix_expr_realise_context) { - // TODO (ca-derivations): add a content-addressed derivation output, which produces a placeholder + // TODO (ca-derivations): add a content-addressing derivation output, which produces a placeholder auto expr = R"( '' a derivation output: ${ @@ -220,7 +222,7 @@ TEST_F(nix_api_expr_test, nix_expr_realise_context) names.push_back(name); } std::sort(names.begin(), names.end()); - ASSERT_EQ(3, names.size()); + ASSERT_EQ(3u, names.size()); EXPECT_THAT(names[0], testing::StrEq("just-a-file")); EXPECT_THAT(names[1], testing::StrEq("letsbuild")); EXPECT_THAT(names[2], testing::StrEq("not-actually-built-yet.drv")); diff --git a/src/libexpr-tests/nix_api_external.cc b/src/libexpr-tests/nix_api_external.cc index 81ff285a4ab..c1deabad687 100644 --- a/src/libexpr-tests/nix_api_external.cc +++ b/src/libexpr-tests/nix_api_external.cc @@ -7,8 +7,8 @@ #include "nix_api_value.h" #include "nix_api_external.h" -#include "tests/nix_api_expr.hh" -#include "tests/string_callback.hh" +#include "nix/expr/tests/nix_api_expr.hh" +#include "nix/util/tests/string_callback.hh" #include @@ -63,6 +63,9 @@ TEST_F(nix_api_expr_test, nix_expr_eval_external) std::string string_value; nix_get_string(nullptr, valueResult, OBSERVE_STRING(string_value)); ASSERT_STREQ("nix-external", string_value.c_str()); + + nix_state_free(stateResult); + nix_state_free(stateFn); } } diff --git a/src/libexpr-tests/nix_api_value.cc b/src/libexpr-tests/nix_api_value.cc index 7fc8b4f641f..1da980ab874 100644 --- a/src/libexpr-tests/nix_api_value.cc +++ b/src/libexpr-tests/nix_api_value.cc @@ -6,10 +6,10 @@ #include "nix_api_value.h" #include "nix_api_expr_internal.h" -#include "tests/nix_api_expr.hh" -#include "tests/string_callback.hh" +#include "nix/expr/tests/nix_api_expr.hh" +#include "nix/util/tests/string_callback.hh" -#include "gmock/gmock.h" +#include #include #include #include @@ -134,12 +134,12 @@ TEST_F(nix_api_expr_test, nix_build_and_init_list_invalid) { ASSERT_EQ(nullptr, nix_get_list_byidx(ctx, nullptr, state, 0)); assert_ctx_err(); - ASSERT_EQ(0, nix_get_list_size(ctx, nullptr)); + ASSERT_EQ(0u, nix_get_list_size(ctx, nullptr)); assert_ctx_err(); ASSERT_EQ(nullptr, nix_get_list_byidx(ctx, value, state, 0)); assert_ctx_err(); - ASSERT_EQ(0, nix_get_list_size(ctx, value)); + ASSERT_EQ(0u, nix_get_list_size(ctx, value)); assert_ctx_err(); } @@ -163,7 +163,7 @@ TEST_F(nix_api_expr_test, nix_build_and_init_list) ASSERT_EQ(42, nix_get_int(ctx, nix_get_list_byidx(ctx, value, state, 0))); ASSERT_EQ(43, nix_get_int(ctx, nix_get_list_byidx(ctx, value, state, 1))); ASSERT_EQ(nullptr, nix_get_list_byidx(ctx, value, state, 2)); - ASSERT_EQ(10, nix_get_list_size(ctx, value)); + ASSERT_EQ(10u, nix_get_list_size(ctx, value)); ASSERT_STREQ("a list", nix_get_typename(ctx, value)); ASSERT_EQ(NIX_TYPE_LIST, nix_get_type(ctx, value)); @@ -180,7 +180,7 @@ TEST_F(nix_api_expr_test, nix_build_and_init_attr_invalid) assert_ctx_err(); ASSERT_EQ(nullptr, nix_get_attr_name_byidx(ctx, nullptr, state, 0)); assert_ctx_err(); - ASSERT_EQ(0, nix_get_attrs_size(ctx, nullptr)); + ASSERT_EQ(0u, nix_get_attrs_size(ctx, nullptr)); assert_ctx_err(); ASSERT_EQ(false, nix_has_attr_byname(ctx, nullptr, state, "no-value")); assert_ctx_err(); @@ -191,7 +191,7 @@ TEST_F(nix_api_expr_test, nix_build_and_init_attr_invalid) assert_ctx_err(); ASSERT_EQ(nullptr, nix_get_attr_name_byidx(ctx, value, state, 0)); assert_ctx_err(); - ASSERT_EQ(0, nix_get_attrs_size(ctx, value)); + ASSERT_EQ(0u, nix_get_attrs_size(ctx, value)); assert_ctx_err(); ASSERT_EQ(false, nix_has_attr_byname(ctx, value, state, "no-value")); assert_ctx_err(); @@ -215,7 +215,7 @@ TEST_F(nix_api_expr_test, nix_build_and_init_attr) nix_make_attrs(ctx, value, builder); nix_bindings_builder_free(builder); - ASSERT_EQ(2, nix_get_attrs_size(ctx, value)); + ASSERT_EQ(2u, nix_get_attrs_size(ctx, value)); nix_value * out_value = nix_get_attr_byname(ctx, value, state, "a"); ASSERT_EQ(42, nix_get_int(ctx, out_value)); @@ -374,7 +374,7 @@ TEST_F(nix_api_expr_test, nix_value_init_apply_lazy_arg) auto n = nix_get_attrs_size(ctx, r); assert_ctx_ok(); - ASSERT_EQ(1, n); + ASSERT_EQ(1u, n); // nix_get_attr_byname isn't lazy (it could have been) so it will throw the exception nix_value * foo = nix_get_attr_byname(ctx, r, state, "foo"); diff --git a/src/libexpr-tests/package.nix b/src/libexpr-tests/package.nix index 959d6b84efd..51d52e935bf 100644 --- a/src/libexpr-tests/package.nix +++ b/src/libexpr-tests/package.nix @@ -1,20 +1,21 @@ -{ lib -, buildPackages -, stdenv -, mkMesonExecutable +{ + lib, + buildPackages, + stdenv, + mkMesonExecutable, -, nix-expr -, nix-expr-c -, nix-expr-test-support + nix-expr, + nix-expr-c, + nix-expr-test-support, -, rapidcheck -, gtest -, runCommand + rapidcheck, + gtest, + runCommand, -# Configuration Options + # Configuration Options -, version -, resolvePath + version, + resolvePath, }: let @@ -27,8 +28,8 @@ mkMesonExecutable (finalAttrs: { workDir = ./.; fileset = fileset.unions [ - ../../build-utils-meson - ./build-utils-meson + ../../nix-meson-build-support + ./nix-meson-build-support ../../.version ./.version ./meson.build @@ -45,33 +46,27 @@ mkMesonExecutable (finalAttrs: { gtest ]; - preConfigure = - # "Inline" .version so it's not a symlink, and includes the suffix. - # Do the meson utils, without modification. - '' - chmod u+w ./.version - echo ${version} > ../../.version - ''; - mesonFlags = [ ]; - env = lib.optionalAttrs (stdenv.isLinux && !(stdenv.hostPlatform.isStatic && stdenv.system == "aarch64-linux")) { - LDFLAGS = "-fuse-ld=gold"; - }; - passthru = { tests = { - run = runCommand "${finalAttrs.pname}-run" { - meta.broken = !stdenv.hostPlatform.emulatorAvailable buildPackages; - } (lib.optionalString stdenv.hostPlatform.isWindows '' - export HOME="$PWD/home-dir" - mkdir -p "$HOME" - '' + '' - export _NIX_TEST_UNIT_DATA=${resolvePath ./data} - ${stdenv.hostPlatform.emulator buildPackages} ${lib.getExe finalAttrs.finalPackage} - touch $out - ''); + run = + runCommand "${finalAttrs.pname}-run" + { + meta.broken = !stdenv.hostPlatform.emulatorAvailable buildPackages; + } + ( + lib.optionalString stdenv.hostPlatform.isWindows '' + export HOME="$PWD/home-dir" + mkdir -p "$HOME" + '' + + '' + export _NIX_TEST_UNIT_DATA=${resolvePath ./data} + ${stdenv.hostPlatform.emulator buildPackages} ${lib.getExe finalAttrs.finalPackage} + touch $out + '' + ); }; }; diff --git a/src/libexpr-tests/primops.cc b/src/libexpr-tests/primops.cc index 5b589823798..9b5590d8d03 100644 --- a/src/libexpr-tests/primops.cc +++ b/src/libexpr-tests/primops.cc @@ -1,10 +1,10 @@ #include #include -#include "eval-settings.hh" -#include "memory-source-accessor.hh" +#include "nix/expr/eval-settings.hh" +#include "nix/util/memory-source-accessor.hh" -#include "tests/libexpr.hh" +#include "nix/expr/tests/libexpr.hh" namespace nix { class CaptureLogger : public Logger @@ -28,20 +28,15 @@ namespace nix { }; class CaptureLogging { - Logger * oldLogger; - std::unique_ptr tempLogger; + std::unique_ptr oldLogger; public: - CaptureLogging() : tempLogger(std::make_unique()) { - oldLogger = logger; - logger = tempLogger.get(); + CaptureLogging() { + oldLogger = std::move(logger); + logger = std::make_unique(); } ~CaptureLogging() { - logger = oldLogger; - } - - std::string get() const { - return tempLogger->get(); + logger = std::move(oldLogger); } }; @@ -61,11 +56,31 @@ namespace nix { TEST_F(PrimOpTest, ceil) { auto v = eval("builtins.ceil 1.9"); ASSERT_THAT(v, IsIntEq(2)); + auto intMin = eval("builtins.ceil (-4611686018427387904 - 4611686018427387904)"); + ASSERT_THAT(intMin, IsIntEq(std::numeric_limits::min())); + ASSERT_THROW(eval("builtins.ceil 1.0e200"), EvalError); + ASSERT_THROW(eval("builtins.ceil -1.0e200"), EvalError); + ASSERT_THROW(eval("builtins.ceil (1.0e200 * 1.0e200)"), EvalError); // inf + ASSERT_THROW(eval("builtins.ceil (-1.0e200 * 1.0e200)"), EvalError); // -inf + ASSERT_THROW(eval("builtins.ceil (1.0e200 * 1.0e200 - 1.0e200 * 1.0e200)"), EvalError); // nan + // bugs in previous Nix versions + ASSERT_THROW(eval("builtins.ceil (4611686018427387904 + 4611686018427387903)"), EvalError); + ASSERT_THROW(eval("builtins.ceil (-4611686018427387904 - 4611686018427387903)"), EvalError); } TEST_F(PrimOpTest, floor) { auto v = eval("builtins.floor 1.9"); ASSERT_THAT(v, IsIntEq(1)); + auto intMin = eval("builtins.ceil (-4611686018427387904 - 4611686018427387904)"); + ASSERT_THAT(intMin, IsIntEq(std::numeric_limits::min())); + ASSERT_THROW(eval("builtins.ceil 1.0e200"), EvalError); + ASSERT_THROW(eval("builtins.ceil -1.0e200"), EvalError); + ASSERT_THROW(eval("builtins.ceil (1.0e200 * 1.0e200)"), EvalError); // inf + ASSERT_THROW(eval("builtins.ceil (-1.0e200 * 1.0e200)"), EvalError); // -inf + ASSERT_THROW(eval("builtins.ceil (1.0e200 * 1.0e200 - 1.0e200 * 1.0e200)"), EvalError); // nan + // bugs in previous Nix versions + ASSERT_THROW(eval("builtins.ceil (4611686018427387904 + 4611686018427387903)"), EvalError); + ASSERT_THROW(eval("builtins.ceil (-4611686018427387904 - 4611686018427387903)"), EvalError); } TEST_F(PrimOpTest, tryEvalFailure) { @@ -113,7 +128,7 @@ namespace nix { CaptureLogging l; auto v = eval("builtins.trace \"test string 123\" 123"); ASSERT_THAT(v, IsIntEq(123)); - auto text = l.get(); + auto text = (dynamic_cast(logger.get()))->get(); ASSERT_NE(text.find("test string 123"), std::string::npos); } @@ -135,8 +150,8 @@ namespace nix { TEST_F(PrimOpTest, attrValues) { auto v = eval("builtins.attrValues { x = \"foo\"; a = 1; }"); ASSERT_THAT(v, IsListOfSize(2)); - ASSERT_THAT(*v.listElems()[0], IsIntEq(1)); - ASSERT_THAT(*v.listElems()[1], IsStringEq("foo")); + ASSERT_THAT(*v.listView()[0], IsIntEq(1)); + ASSERT_THAT(*v.listView()[1], IsStringEq("foo")); } TEST_F(PrimOpTest, getAttr) { @@ -209,7 +224,7 @@ namespace nix { auto v = eval("builtins.listToAttrs []"); ASSERT_THAT(v, IsAttrsOfSize(0)); ASSERT_EQ(v.type(), nAttrs); - ASSERT_EQ(v.attrs()->size(), 0); + ASSERT_EQ(v.attrs()->size(), 0u); } TEST_F(PrimOpTest, listToAttrsNotFieldName) { @@ -235,8 +250,8 @@ namespace nix { TEST_F(PrimOpTest, catAttrs) { auto v = eval("builtins.catAttrs \"a\" [{a = 1;} {b = 0;} {a = 2;}]"); ASSERT_THAT(v, IsListOfSize(2)); - ASSERT_THAT(*v.listElems()[0], IsIntEq(1)); - ASSERT_THAT(*v.listElems()[1], IsIntEq(2)); + ASSERT_THAT(*v.listView()[0], IsIntEq(1)); + ASSERT_THAT(*v.listView()[1], IsIntEq(2)); } TEST_F(PrimOpTest, functionArgs) { @@ -286,6 +301,7 @@ namespace nix { TEST_F(PrimOpTest, elemtAtOutOfBounds) { ASSERT_THROW(eval("builtins.elemAt [0 1 2 3] 5"), Error); + ASSERT_THROW(eval("builtins.elemAt [0] 4294967296"), Error); } TEST_F(PrimOpTest, head) { @@ -304,7 +320,8 @@ namespace nix { TEST_F(PrimOpTest, tail) { auto v = eval("builtins.tail [ 3 2 1 0 ]"); ASSERT_THAT(v, IsListOfSize(3)); - for (const auto [n, elem] : enumerate(v.listItems())) + auto listView = v.listView(); + for (const auto [n, elem] : enumerate(listView)) ASSERT_THAT(*elem, IsIntEq(2 - static_cast(n))); } @@ -315,17 +332,17 @@ namespace nix { TEST_F(PrimOpTest, map) { auto v = eval("map (x: \"foo\" + x) [ \"bar\" \"bla\" \"abc\" ]"); ASSERT_THAT(v, IsListOfSize(3)); - auto elem = v.listElems()[0]; + auto elem = v.listView()[0]; ASSERT_THAT(*elem, IsThunk()); state.forceValue(*elem, noPos); ASSERT_THAT(*elem, IsStringEq("foobar")); - elem = v.listElems()[1]; + elem = v.listView()[1]; ASSERT_THAT(*elem, IsThunk()); state.forceValue(*elem, noPos); ASSERT_THAT(*elem, IsStringEq("foobla")); - elem = v.listElems()[2]; + elem = v.listView()[2]; ASSERT_THAT(*elem, IsThunk()); state.forceValue(*elem, noPos); ASSERT_THAT(*elem, IsStringEq("fooabc")); @@ -334,7 +351,7 @@ namespace nix { TEST_F(PrimOpTest, filter) { auto v = eval("builtins.filter (x: x == 2) [ 3 2 3 2 3 2 ]"); ASSERT_THAT(v, IsListOfSize(3)); - for (const auto elem : v.listItems()) + for (const auto elem : v.listView()) ASSERT_THAT(*elem, IsIntEq(2)); } @@ -351,7 +368,8 @@ namespace nix { TEST_F(PrimOpTest, concatLists) { auto v = eval("builtins.concatLists [[1 2] [3 4]]"); ASSERT_THAT(v, IsListOfSize(4)); - for (const auto [i, elem] : enumerate(v.listItems())) + auto listView = v.listView(); + for (const auto [i, elem] : enumerate(listView)) ASSERT_THAT(*elem, IsIntEq(static_cast(i)+1)); } @@ -388,8 +406,9 @@ namespace nix { TEST_F(PrimOpTest, genList) { auto v = eval("builtins.genList (x: x + 1) 3"); ASSERT_EQ(v.type(), nList); - ASSERT_EQ(v.listSize(), 3); - for (const auto [i, elem] : enumerate(v.listItems())) { + ASSERT_EQ(v.listSize(), 3u); + auto listView = v.listView(); + for (const auto [i, elem] : enumerate(listView)) { ASSERT_THAT(*elem, IsThunk()); state.forceValue(*elem, noPos); ASSERT_THAT(*elem, IsIntEq(static_cast(i)+1)); @@ -399,10 +418,11 @@ namespace nix { TEST_F(PrimOpTest, sortLessThan) { auto v = eval("builtins.sort builtins.lessThan [ 483 249 526 147 42 77 ]"); ASSERT_EQ(v.type(), nList); - ASSERT_EQ(v.listSize(), 6); + ASSERT_EQ(v.listSize(), 6u); const std::vector numbers = { 42, 77, 147, 249, 483, 526 }; - for (const auto [n, elem] : enumerate(v.listItems())) + auto listView = v.listView(); + for (const auto [n, elem] : enumerate(listView)) ASSERT_THAT(*elem, IsIntEq(numbers[n])); } @@ -413,26 +433,27 @@ namespace nix { auto right = v.attrs()->get(createSymbol("right")); ASSERT_NE(right, nullptr); ASSERT_THAT(*right->value, IsListOfSize(2)); - ASSERT_THAT(*right->value->listElems()[0], IsIntEq(23)); - ASSERT_THAT(*right->value->listElems()[1], IsIntEq(42)); + ASSERT_THAT(*right->value->listView()[0], IsIntEq(23)); + ASSERT_THAT(*right->value->listView()[1], IsIntEq(42)); auto wrong = v.attrs()->get(createSymbol("wrong")); ASSERT_NE(wrong, nullptr); ASSERT_EQ(wrong->value->type(), nList); - ASSERT_EQ(wrong->value->listSize(), 3); + ASSERT_EQ(wrong->value->listSize(), 3u); ASSERT_THAT(*wrong->value, IsListOfSize(3)); - ASSERT_THAT(*wrong->value->listElems()[0], IsIntEq(1)); - ASSERT_THAT(*wrong->value->listElems()[1], IsIntEq(9)); - ASSERT_THAT(*wrong->value->listElems()[2], IsIntEq(3)); + ASSERT_THAT(*wrong->value->listView()[0], IsIntEq(1)); + ASSERT_THAT(*wrong->value->listView()[1], IsIntEq(9)); + ASSERT_THAT(*wrong->value->listView()[2], IsIntEq(3)); } TEST_F(PrimOpTest, concatMap) { auto v = eval("builtins.concatMap (x: x ++ [0]) [ [1 2] [3 4] ]"); ASSERT_EQ(v.type(), nList); - ASSERT_EQ(v.listSize(), 6); + ASSERT_EQ(v.listSize(), 6u); const std::vector numbers = { 1, 2, 0, 3, 4, 0 }; - for (const auto [n, elem] : enumerate(v.listItems())) + auto listView = v.listView(); + for (const auto [n, elem] : enumerate(listView)) ASSERT_THAT(*elem, IsIntEq(numbers[n])); } @@ -577,6 +598,16 @@ namespace nix { ASSERT_THAT(v, IsStringEq("n")); } + TEST_F(PrimOpTest, substringHugeStart){ + auto v = eval("builtins.substring 4294967296 5 \"nixos\""); + ASSERT_THAT(v, IsStringEq("")); + } + + TEST_F(PrimOpTest, substringHugeLength){ + auto v = eval("builtins.substring 0 4294967296 \"nixos\""); + ASSERT_THAT(v, IsStringEq("nixos")); + } + TEST_F(PrimOpTest, substringEmptyString){ auto v = eval("builtins.substring 1 3 \"\""); ASSERT_THAT(v, IsStringEq("")); @@ -641,8 +672,8 @@ namespace nix { auto v = eval("derivation"); ASSERT_EQ(v.type(), nFunction); ASSERT_TRUE(v.isLambda()); - ASSERT_NE(v.payload.lambda.fun, nullptr); - ASSERT_TRUE(v.payload.lambda.fun->hasFormals()); + ASSERT_NE(v.lambda().fun, nullptr); + ASSERT_TRUE(v.lambda().fun->hasFormals()); } TEST_F(PrimOpTest, currentTime) { @@ -656,7 +687,8 @@ namespace nix { ASSERT_THAT(v, IsListOfSize(4)); const std::vector strings = { "1", "2", "3", "git" }; - for (const auto [n, p] : enumerate(v.listItems())) + auto listView = v.listView(); + for (const auto [n, p] : enumerate(listView)) ASSERT_THAT(*p, IsStringEq(strings[n])); } @@ -746,12 +778,12 @@ namespace nix { auto v = eval("builtins.split \"(a)b\" \"abc\""); ASSERT_THAT(v, IsListOfSize(3)); - ASSERT_THAT(*v.listElems()[0], IsStringEq("")); + ASSERT_THAT(*v.listView()[0], IsStringEq("")); - ASSERT_THAT(*v.listElems()[1], IsListOfSize(1)); - ASSERT_THAT(*v.listElems()[1]->listElems()[0], IsStringEq("a")); + ASSERT_THAT(*v.listView()[1], IsListOfSize(1)); + ASSERT_THAT(*v.listView()[1]->listView()[0], IsStringEq("a")); - ASSERT_THAT(*v.listElems()[2], IsStringEq("c")); + ASSERT_THAT(*v.listView()[2], IsStringEq("c")); } TEST_F(PrimOpTest, split2) { @@ -759,17 +791,17 @@ namespace nix { auto v = eval("builtins.split \"([ac])\" \"abc\""); ASSERT_THAT(v, IsListOfSize(5)); - ASSERT_THAT(*v.listElems()[0], IsStringEq("")); + ASSERT_THAT(*v.listView()[0], IsStringEq("")); - ASSERT_THAT(*v.listElems()[1], IsListOfSize(1)); - ASSERT_THAT(*v.listElems()[1]->listElems()[0], IsStringEq("a")); + ASSERT_THAT(*v.listView()[1], IsListOfSize(1)); + ASSERT_THAT(*v.listView()[1]->listView()[0], IsStringEq("a")); - ASSERT_THAT(*v.listElems()[2], IsStringEq("b")); + ASSERT_THAT(*v.listView()[2], IsStringEq("b")); - ASSERT_THAT(*v.listElems()[3], IsListOfSize(1)); - ASSERT_THAT(*v.listElems()[3]->listElems()[0], IsStringEq("c")); + ASSERT_THAT(*v.listView()[3], IsListOfSize(1)); + ASSERT_THAT(*v.listView()[3]->listView()[0], IsStringEq("c")); - ASSERT_THAT(*v.listElems()[4], IsStringEq("")); + ASSERT_THAT(*v.listView()[4], IsStringEq("")); } TEST_F(PrimOpTest, split3) { @@ -777,36 +809,36 @@ namespace nix { ASSERT_THAT(v, IsListOfSize(5)); // First list element - ASSERT_THAT(*v.listElems()[0], IsStringEq("")); + ASSERT_THAT(*v.listView()[0], IsStringEq("")); // 2nd list element is a list [ "" null ] - ASSERT_THAT(*v.listElems()[1], IsListOfSize(2)); - ASSERT_THAT(*v.listElems()[1]->listElems()[0], IsStringEq("a")); - ASSERT_THAT(*v.listElems()[1]->listElems()[1], IsNull()); + ASSERT_THAT(*v.listView()[1], IsListOfSize(2)); + ASSERT_THAT(*v.listView()[1]->listView()[0], IsStringEq("a")); + ASSERT_THAT(*v.listView()[1]->listView()[1], IsNull()); // 3rd element - ASSERT_THAT(*v.listElems()[2], IsStringEq("b")); + ASSERT_THAT(*v.listView()[2], IsStringEq("b")); // 4th element is a list: [ null "c" ] - ASSERT_THAT(*v.listElems()[3], IsListOfSize(2)); - ASSERT_THAT(*v.listElems()[3]->listElems()[0], IsNull()); - ASSERT_THAT(*v.listElems()[3]->listElems()[1], IsStringEq("c")); + ASSERT_THAT(*v.listView()[3], IsListOfSize(2)); + ASSERT_THAT(*v.listView()[3]->listView()[0], IsNull()); + ASSERT_THAT(*v.listView()[3]->listView()[1], IsStringEq("c")); // 5th element is the empty string - ASSERT_THAT(*v.listElems()[4], IsStringEq("")); + ASSERT_THAT(*v.listView()[4], IsStringEq("")); } TEST_F(PrimOpTest, split4) { auto v = eval("builtins.split \"([[:upper:]]+)\" \" FOO \""); ASSERT_THAT(v, IsListOfSize(3)); - auto first = v.listElems()[0]; - auto second = v.listElems()[1]; - auto third = v.listElems()[2]; + auto first = v.listView()[0]; + auto second = v.listView()[1]; + auto third = v.listView()[2]; ASSERT_THAT(*first, IsStringEq(" ")); ASSERT_THAT(*second, IsListOfSize(1)); - ASSERT_THAT(*second->listElems()[0], IsStringEq("FOO")); + ASSERT_THAT(*second->listView()[0], IsStringEq("FOO")); ASSERT_THAT(*third, IsStringEq(" ")); } @@ -824,14 +856,14 @@ namespace nix { TEST_F(PrimOpTest, match3) { auto v = eval("builtins.match \"a(b)(c)\" \"abc\""); ASSERT_THAT(v, IsListOfSize(2)); - ASSERT_THAT(*v.listElems()[0], IsStringEq("b")); - ASSERT_THAT(*v.listElems()[1], IsStringEq("c")); + ASSERT_THAT(*v.listView()[0], IsStringEq("b")); + ASSERT_THAT(*v.listView()[1], IsStringEq("c")); } TEST_F(PrimOpTest, match4) { auto v = eval("builtins.match \"[[:space:]]+([[:upper:]]+)[[:space:]]+\" \" FOO \""); ASSERT_THAT(v, IsListOfSize(1)); - ASSERT_THAT(*v.listElems()[0], IsStringEq("FOO")); + ASSERT_THAT(*v.listView()[0], IsStringEq("FOO")); } TEST_F(PrimOpTest, match5) { @@ -848,7 +880,8 @@ namespace nix { // ensure that the list is sorted const std::vector expected { "a", "x", "y", "z" }; - for (const auto [n, elem] : enumerate(v.listItems())) + auto listView = v.listView(); + for (const auto [n, elem] : enumerate(listView)) ASSERT_THAT(*elem, IsStringEq(expected[n])); } diff --git a/src/libexpr-tests/search-path.cc b/src/libexpr-tests/search-path.cc index 0806793557d..792bb0812ff 100644 --- a/src/libexpr-tests/search-path.cc +++ b/src/libexpr-tests/search-path.cc @@ -1,7 +1,7 @@ #include #include -#include "search-path.hh" +#include "nix/expr/search-path.hh" namespace nix { diff --git a/src/libexpr-tests/trivial.cc b/src/libexpr-tests/trivial.cc index d77b4d53b47..6eabad6d7a4 100644 --- a/src/libexpr-tests/trivial.cc +++ b/src/libexpr-tests/trivial.cc @@ -1,4 +1,4 @@ -#include "tests/libexpr.hh" +#include "nix/expr/tests/libexpr.hh" namespace nix { // Testing of trivial expressions @@ -143,7 +143,7 @@ namespace nix { // Usually Nix rejects duplicate keys in an attrset but it does allow // so if it is an attribute set that contains disjoint sets of keys. // The below is equivalent to `{a.b = 1; a.c = 2; }`. - // The attribute set `a` will be a Thunk at first as the attribuets + // The attribute set `a` will be a Thunk at first as the attributes // have to be merged (or otherwise computed) and that is done in a lazy // manner. diff --git a/src/libexpr-tests/value/context.cc b/src/libexpr-tests/value/context.cc index 761286dbdcc..97cd50f7554 100644 --- a/src/libexpr-tests/value/context.cc +++ b/src/libexpr-tests/value/context.cc @@ -2,9 +2,9 @@ #include #include -#include "tests/path.hh" -#include "tests/libexpr.hh" -#include "tests/value/context.hh" +#include "nix/store/tests/path.hh" +#include "nix/expr/tests/libexpr.hh" +#include "nix/expr/tests/value/context.hh" namespace nix { @@ -124,7 +124,9 @@ RC_GTEST_PROP( prop_round_rip, (const NixStringContextElem & o)) { - RC_ASSERT(o == NixStringContextElem::parse(o.to_string())); + ExperimentalFeatureSettings xpSettings; + xpSettings.set("experimental-features", "dynamic-derivations"); + RC_ASSERT(o == NixStringContextElem::parse(o.to_string(), xpSettings)); } #endif diff --git a/src/libexpr-tests/value/print.cc b/src/libexpr-tests/value/print.cc index 43b54503546..d337a29a38d 100644 --- a/src/libexpr-tests/value/print.cc +++ b/src/libexpr-tests/value/print.cc @@ -1,7 +1,7 @@ -#include "tests/libexpr.hh" +#include "nix/expr/tests/libexpr.hh" -#include "value.hh" -#include "print.hh" +#include "nix/expr/value.hh" +#include "nix/expr/print.hh" namespace nix { diff --git a/src/libexpr-tests/value/value.cc b/src/libexpr-tests/value/value.cc index 5762d5891f8..63501dd4995 100644 --- a/src/libexpr-tests/value/value.cc +++ b/src/libexpr-tests/value/value.cc @@ -1,6 +1,6 @@ -#include "value.hh" +#include "nix/expr/value.hh" -#include "tests/libstore.hh" +#include "nix/store/tests/libstore.hh" namespace nix { diff --git a/src/libexpr/attr-path.cc b/src/libexpr/attr-path.cc index 822ec7620c2..111d04cf2c0 100644 --- a/src/libexpr/attr-path.cc +++ b/src/libexpr/attr-path.cc @@ -1,5 +1,5 @@ -#include "attr-path.hh" -#include "eval-inline.hh" +#include "nix/expr/attr-path.hh" +#include "nix/expr/eval-inline.hh" namespace nix { @@ -74,7 +74,7 @@ std::pair findAlongAttrPath(EvalState & state, const std::strin auto a = v->attrs()->get(state.symbols.create(attr)); if (!a) { - std::set attrNames; + StringSet attrNames; for (auto & attr : *v->attrs()) attrNames.insert(std::string(state.symbols[attr.name])); @@ -95,7 +95,7 @@ std::pair findAlongAttrPath(EvalState & state, const std::strin if (*attrIndex >= v->listSize()) throw AttrPathNotFound("list index %1% in selection path '%2%' is out of range", *attrIndex, attrPath); - v = v->listElems()[*attrIndex]; + v = v->listView()[*attrIndex]; pos = noPos; } diff --git a/src/libexpr/attr-set.cc b/src/libexpr/attr-set.cc index 866ef817aa4..06e245aea6b 100644 --- a/src/libexpr/attr-set.cc +++ b/src/libexpr/attr-set.cc @@ -1,5 +1,5 @@ -#include "attr-set.hh" -#include "eval-inline.hh" +#include "nix/expr/attr-set.hh" +#include "nix/expr/eval-inline.hh" #include diff --git a/src/libexpr/build-utils-meson b/src/libexpr/build-utils-meson deleted file mode 120000 index 5fff21bab55..00000000000 --- a/src/libexpr/build-utils-meson +++ /dev/null @@ -1 +0,0 @@ -../../build-utils-meson \ No newline at end of file diff --git a/src/libexpr/call-flake.nix b/src/libexpr/call-flake.nix deleted file mode 100644 index a008346e551..00000000000 --- a/src/libexpr/call-flake.nix +++ /dev/null @@ -1,89 +0,0 @@ -# This is a helper to callFlake() to lazily fetch flake inputs. - -# The contents of the lock file, in JSON format. -lockFileStr: - -# A mapping of lock file node IDs to { sourceInfo, subdir } attrsets, -# with sourceInfo.outPath providing an SourceAccessor to a previously -# fetched tree. This is necessary for possibly unlocked inputs, in -# particular the root input, but also --override-inputs pointing to -# unlocked trees. -overrides: - -# This is `prim_fetchFinalTree`. -fetchTreeFinal: - -let - - lockFile = builtins.fromJSON lockFileStr; - - # Resolve a input spec into a node name. An input spec is - # either a node name, or a 'follows' path from the root - # node. - resolveInput = inputSpec: - if builtins.isList inputSpec - then getInputByPath lockFile.root inputSpec - else inputSpec; - - # Follow an input path (e.g. ["dwarffs" "nixpkgs"]) from the - # root node, returning the final node. - getInputByPath = nodeName: path: - if path == [] - then nodeName - else - getInputByPath - # Since this could be a 'follows' input, call resolveInput. - (resolveInput lockFile.nodes.${nodeName}.inputs.${builtins.head path}) - (builtins.tail path); - - allNodes = - builtins.mapAttrs - (key: node: - let - - sourceInfo = - if overrides ? ${key} - then - overrides.${key}.sourceInfo - else - # FIXME: remove obsolete node.info. - # Note: lock file entries are always final. - fetchTreeFinal (node.info or {} // removeAttrs node.locked ["dir"]); - - subdir = overrides.${key}.dir or node.locked.dir or ""; - - outPath = sourceInfo + ((if subdir == "" then "" else "/") + subdir); - - flake = import (outPath + "/flake.nix"); - - inputs = builtins.mapAttrs - (inputName: inputSpec: allNodes.${resolveInput inputSpec}) - (node.inputs or {}); - - outputs = flake.outputs (inputs // { self = result; }); - - result = - outputs - # We add the sourceInfo attribute for its metadata, as they are - # relevant metadata for the flake. However, the outPath of the - # sourceInfo does not necessarily match the outPath of the flake, - # as the flake may be in a subdirectory of a source. - # This is shadowed in the next // - // sourceInfo - // { - # This shadows the sourceInfo.outPath - inherit outPath; - - inherit inputs; inherit outputs; inherit sourceInfo; _type = "flake"; - }; - - in - if node.flake or true then - assert builtins.isFunction flake.outputs; - result - else - sourceInfo - ) - lockFile.nodes; - -in allNodes.${lockFile.root} diff --git a/src/libexpr/eval-cache.cc b/src/libexpr/eval-cache.cc index ea3319f9939..27d60d6ef49 100644 --- a/src/libexpr/eval-cache.cc +++ b/src/libexpr/eval-cache.cc @@ -1,11 +1,11 @@ -#include "users.hh" -#include "eval-cache.hh" -#include "sqlite.hh" -#include "eval.hh" -#include "eval-inline.hh" -#include "store-api.hh" +#include "nix/util/users.hh" +#include "nix/expr/eval-cache.hh" +#include "nix/store/sqlite.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/store/store-api.hh" // Need specialization involving `SymbolStr` just in this one module. -#include "strings-inline.hh" +#include "nix/util/strings-inline.hh" namespace nix::eval_cache { @@ -418,6 +418,14 @@ Value & AttrCursor::getValue() return **_value; } +void AttrCursor::fetchCachedValue() +{ + if (!cachedValue) + cachedValue = root->db->getAttr(getKey()); + if (cachedValue && std::get_if(&cachedValue->second) && parent) + throw CachedEvalError(parent->first, parent->second); +} + std::vector AttrCursor::getAttrPath() const { if (parent) { @@ -484,7 +492,7 @@ Value & AttrCursor::forceValue() Suggestions AttrCursor::getSuggestionsForAttr(Symbol name) { auto attrNames = getAttrs(); - std::set strAttrNames; + StringSet strAttrNames; for (auto & name : attrNames) strAttrNames.insert(std::string(root->state.symbols[name])); @@ -494,14 +502,13 @@ Suggestions AttrCursor::getSuggestionsForAttr(Symbol name) std::shared_ptr AttrCursor::maybeGetAttr(Symbol name) { if (root->db) { - if (!cachedValue) - cachedValue = root->db->getAttr(getKey()); + fetchCachedValue(); if (cachedValue) { if (auto attrs = std::get_if>(&cachedValue->second)) { for (auto & attr : *attrs) if (attr == name) - return std::make_shared(root, std::make_pair(shared_from_this(), attr)); + return std::make_shared(root, std::make_pair(ref(shared_from_this()), attr)); return nullptr; } else if (std::get_if(&cachedValue->second)) { auto attr = root->db->getAttr({cachedValue->first, name}); @@ -512,7 +519,7 @@ std::shared_ptr AttrCursor::maybeGetAttr(Symbol name) throw CachedEvalError(ref(shared_from_this()), name); else return std::make_shared(root, - std::make_pair(shared_from_this(), name), nullptr, std::move(attr)); + std::make_pair(ref(shared_from_this()), name), nullptr, std::move(attr)); } // Incomplete attrset, so need to fall thru and // evaluate to see whether 'name' exists @@ -547,7 +554,7 @@ std::shared_ptr AttrCursor::maybeGetAttr(Symbol name) } return make_ref( - root, std::make_pair(shared_from_this(), name), attr->value, std::move(cachedValue2)); + root, std::make_pair(ref(shared_from_this()), name), attr->value, std::move(cachedValue2)); } std::shared_ptr AttrCursor::maybeGetAttr(std::string_view name) @@ -585,8 +592,7 @@ OrSuggestions> AttrCursor::findAlongAttrPath(const std::vectordb) { - if (!cachedValue) - cachedValue = root->db->getAttr(getKey()); + fetchCachedValue(); if (cachedValue && !std::get_if(&cachedValue->second)) { if (auto s = std::get_if(&cachedValue->second)) { debug("using cached string attribute '%s'", getAttrPathStr()); @@ -607,8 +613,7 @@ std::string AttrCursor::getString() string_t AttrCursor::getStringWithContext() { if (root->db) { - if (!cachedValue) - cachedValue = root->db->getAttr(getKey()); + fetchCachedValue(); if (cachedValue && !std::get_if(&cachedValue->second)) { if (auto s = std::get_if(&cachedValue->second)) { bool valid = true; @@ -654,8 +659,7 @@ string_t AttrCursor::getStringWithContext() bool AttrCursor::getBool() { if (root->db) { - if (!cachedValue) - cachedValue = root->db->getAttr(getKey()); + fetchCachedValue(); if (cachedValue && !std::get_if(&cachedValue->second)) { if (auto b = std::get_if(&cachedValue->second)) { debug("using cached Boolean attribute '%s'", getAttrPathStr()); @@ -676,8 +680,7 @@ bool AttrCursor::getBool() NixInt AttrCursor::getInt() { if (root->db) { - if (!cachedValue) - cachedValue = root->db->getAttr(getKey()); + fetchCachedValue(); if (cachedValue && !std::get_if(&cachedValue->second)) { if (auto i = std::get_if(&cachedValue->second)) { debug("using cached integer attribute '%s'", getAttrPathStr()); @@ -698,8 +701,7 @@ NixInt AttrCursor::getInt() std::vector AttrCursor::getListOfStrings() { if (root->db) { - if (!cachedValue) - cachedValue = root->db->getAttr(getKey()); + fetchCachedValue(); if (cachedValue && !std::get_if(&cachedValue->second)) { if (auto l = std::get_if>(&cachedValue->second)) { debug("using cached list of strings attribute '%s'", getAttrPathStr()); @@ -719,7 +721,7 @@ std::vector AttrCursor::getListOfStrings() std::vector res; - for (auto & elem : v.listItems()) + for (auto elem : v.listView()) res.push_back(std::string(root->state.forceStringNoCtx(*elem, noPos, "while evaluating an attribute for caching"))); if (root->db) @@ -731,8 +733,7 @@ std::vector AttrCursor::getListOfStrings() std::vector AttrCursor::getAttrs() { if (root->db) { - if (!cachedValue) - cachedValue = root->db->getAttr(getKey()); + fetchCachedValue(); if (cachedValue && !std::get_if(&cachedValue->second)) { if (auto attrs = std::get_if>(&cachedValue->second)) { debug("using cached attrset attribute '%s'", getAttrPathStr()); diff --git a/src/libexpr/eval-error.cc b/src/libexpr/eval-error.cc index cdb0b477242..eac13500803 100644 --- a/src/libexpr/eval-error.cc +++ b/src/libexpr/eval-error.cc @@ -1,6 +1,6 @@ -#include "eval-error.hh" -#include "eval.hh" -#include "value.hh" +#include "nix/expr/eval-error.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/value.hh" namespace nix { @@ -45,7 +45,7 @@ EvalErrorBuilder & EvalErrorBuilder::withFrame(const Env & env, const Expr // TODO: check compatibility with nested debugger calls. // TODO: What side-effects?? error.state.debugTraces.push_front(DebugTrace{ - .pos = error.state.positions[expr.getPos()], + .pos = expr.getPos(), .expr = expr, .env = env, .hint = HintFmt("Fake frame for debugging purposes"), @@ -110,5 +110,6 @@ template class EvalErrorBuilder; template class EvalErrorBuilder; template class EvalErrorBuilder; template class EvalErrorBuilder; +template class EvalErrorBuilder; } diff --git a/src/libexpr/eval-gc.cc b/src/libexpr/eval-gc.cc index 07ce05a2c73..5a4ecf03575 100644 --- a/src/libexpr/eval-gc.cc +++ b/src/libexpr/eval-gc.cc @@ -1,14 +1,17 @@ -#include "error.hh" -#include "environment-variables.hh" -#include "eval-settings.hh" -#include "config-global.hh" -#include "serialise.hh" -#include "eval-gc.hh" +#include "nix/util/error.hh" +#include "nix/util/environment-variables.hh" +#include "nix/expr/eval-settings.hh" +#include "nix/util/config-global.hh" +#include "nix/util/serialise.hh" +#include "nix/expr/eval-gc.hh" +#include "nix/expr/value.hh" -#if HAVE_BOEHMGC +#include "expr-config-private.hh" + +#if NIX_USE_BOEHMGC # include -# if __FreeBSD__ +# ifdef __FreeBSD__ # include # endif @@ -24,7 +27,7 @@ namespace nix { -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC /* Called when the Boehm GC runs out of memory. */ static void * oomHandler(size_t requested) { @@ -50,6 +53,13 @@ static inline void initGCReal() GC_INIT(); + /* Register valid displacements in case we are using alignment niches + for storing the type information. This way tagged pointers are considered + to be valid, even when they are not aligned. */ + if constexpr (detail::useBitPackedValueStorage) + for (std::size_t i = 1; i < sizeof(std::uintptr_t); ++i) + GC_register_displacement(i); + GC_set_oom_fn(oomHandler); /* Set the initial heap size to something fairly big (25% of @@ -94,7 +104,7 @@ void initGC() if (gcInitialised) return; -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC initGCReal(); gcCyclesAfterInit = GC_get_gc_no(); diff --git a/src/libexpr/eval-profiler-settings.cc b/src/libexpr/eval-profiler-settings.cc new file mode 100644 index 00000000000..1a35d4a2d11 --- /dev/null +++ b/src/libexpr/eval-profiler-settings.cc @@ -0,0 +1,49 @@ +#include "nix/expr/eval-profiler-settings.hh" +#include "nix/util/configuration.hh" +#include "nix/util/logging.hh" /* Needs to be included before config-impl.hh */ +#include "nix/util/config-impl.hh" +#include "nix/util/abstract-setting-to-json.hh" + +#include + +namespace nix { + +template<> +EvalProfilerMode BaseSetting::parse(const std::string & str) const +{ + if (str == "disabled") + return EvalProfilerMode::disabled; + else if (str == "flamegraph") + return EvalProfilerMode::flamegraph; + else + throw UsageError("option '%s' has invalid value '%s'", name, str); +} + +template<> +struct BaseSetting::trait +{ + static constexpr bool appendable = false; +}; + +template<> +std::string BaseSetting::to_string() const +{ + if (value == EvalProfilerMode::disabled) + return "disabled"; + else if (value == EvalProfilerMode::flamegraph) + return "flamegraph"; + else + unreachable(); +} + +NLOHMANN_JSON_SERIALIZE_ENUM( + EvalProfilerMode, + { + {EvalProfilerMode::disabled, "disabled"}, + {EvalProfilerMode::flamegraph, "flamegraph"}, + }); + +/* Explicit instantiation of templates */ +template class BaseSetting; + +} diff --git a/src/libexpr/eval-profiler.cc b/src/libexpr/eval-profiler.cc new file mode 100644 index 00000000000..b65bc3a4d45 --- /dev/null +++ b/src/libexpr/eval-profiler.cc @@ -0,0 +1,355 @@ +#include "nix/expr/eval-profiler.hh" +#include "nix/expr/nixexpr.hh" +#include "nix/expr/eval.hh" +#include "nix/util/lru-cache.hh" + +namespace nix { + +void EvalProfiler::preFunctionCallHook(EvalState & state, const Value & v, std::span args, const PosIdx pos) {} + +void EvalProfiler::postFunctionCallHook(EvalState & state, const Value & v, std::span args, const PosIdx pos) +{ +} + +void MultiEvalProfiler::preFunctionCallHook( + EvalState & state, const Value & v, std::span args, const PosIdx pos) +{ + for (auto & profiler : profilers) { + if (profiler->getNeededHooks().test(Hook::preFunctionCall)) + profiler->preFunctionCallHook(state, v, args, pos); + } +} + +void MultiEvalProfiler::postFunctionCallHook( + EvalState & state, const Value & v, std::span args, const PosIdx pos) +{ + for (auto & profiler : profilers) { + if (profiler->getNeededHooks().test(Hook::postFunctionCall)) + profiler->postFunctionCallHook(state, v, args, pos); + } +} + +EvalProfiler::Hooks MultiEvalProfiler::getNeededHooksImpl() const +{ + Hooks hooks; + for (auto & p : profilers) + hooks |= p->getNeededHooks(); + return hooks; +} + +void MultiEvalProfiler::addProfiler(ref profiler) +{ + profilers.push_back(profiler); + invalidateNeededHooks(); +} + +namespace { + +class PosCache : private LRUCache +{ + const EvalState & state; + +public: + PosCache(const EvalState & state) + : LRUCache(524288) /* ~40MiB */ + , state(state) + { + } + + Pos lookup(PosIdx posIdx) + { + auto posOrNone = LRUCache::get(posIdx); + if (posOrNone) + return *posOrNone; + + auto pos = state.positions[posIdx]; + upsert(posIdx, pos); + return pos; + } +}; + +struct LambdaFrameInfo +{ + ExprLambda * expr; + /** Position where the lambda has been called. */ + PosIdx callPos = noPos; + std::ostream & symbolize(const EvalState & state, std::ostream & os, PosCache & posCache) const; + auto operator<=>(const LambdaFrameInfo & rhs) const = default; +}; + +/** Primop call. */ +struct PrimOpFrameInfo +{ + const PrimOp * expr; + /** Position where the primop has been called. */ + PosIdx callPos = noPos; + std::ostream & symbolize(const EvalState & state, std::ostream & os, PosCache & posCache) const; + auto operator<=>(const PrimOpFrameInfo & rhs) const = default; +}; + +/** Used for functor calls (attrset with __functor attr). */ +struct FunctorFrameInfo +{ + PosIdx pos; + std::ostream & symbolize(const EvalState & state, std::ostream & os, PosCache & posCache) const; + auto operator<=>(const FunctorFrameInfo & rhs) const = default; +}; + +struct DerivationStrictFrameInfo +{ + PosIdx callPos = noPos; + std::string drvName; + std::ostream & symbolize(const EvalState & state, std::ostream & os, PosCache & posCache) const; + auto operator<=>(const DerivationStrictFrameInfo & rhs) const = default; +}; + +/** Fallback frame info. */ +struct GenericFrameInfo +{ + PosIdx pos; + std::ostream & symbolize(const EvalState & state, std::ostream & os, PosCache & posCache) const; + auto operator<=>(const GenericFrameInfo & rhs) const = default; +}; + +using FrameInfo = + std::variant; +using FrameStack = std::vector; + +/** + * Stack sampling profiler. + */ +class SampleStack : public EvalProfiler +{ + /* How often stack profiles should be flushed to file. This avoids the need + to persist stack samples across the whole evaluation at the cost + of periodically flushing data to disk. */ + static constexpr std::chrono::microseconds profileDumpInterval = std::chrono::milliseconds(2000); + + Hooks getNeededHooksImpl() const override + { + return Hooks().set(preFunctionCall).set(postFunctionCall); + } + + FrameInfo getPrimOpFrameInfo(const PrimOp & primOp, std::span args, PosIdx pos); + +public: + SampleStack(EvalState & state, std::filesystem::path profileFile, std::chrono::nanoseconds period) + : state(state) + , sampleInterval(period) + , profileFd([&]() { + AutoCloseFD fd = toDescriptor(open(profileFile.string().c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0660)); + if (!fd) + throw SysError("opening file %s", profileFile); + return fd; + }()) + , posCache(state) + { + } + + [[gnu::noinline]] void + preFunctionCallHook(EvalState & state, const Value & v, std::span args, const PosIdx pos) override; + [[gnu::noinline]] void + postFunctionCallHook(EvalState & state, const Value & v, std::span args, const PosIdx pos) override; + + void maybeSaveProfile(std::chrono::time_point now); + void saveProfile(); + FrameInfo getFrameInfoFromValueAndPos(const Value & v, std::span args, PosIdx pos); + + SampleStack(SampleStack &&) = default; + SampleStack & operator=(SampleStack &&) = delete; + SampleStack(const SampleStack &) = delete; + SampleStack & operator=(const SampleStack &) = delete; + ~SampleStack(); +private: + /** Hold on to an instance of EvalState for symbolizing positions. */ + EvalState & state; + std::chrono::nanoseconds sampleInterval; + AutoCloseFD profileFd; + FrameStack stack; + std::map callCount; + std::chrono::time_point lastStackSample = + std::chrono::high_resolution_clock::now(); + std::chrono::time_point lastDump = std::chrono::high_resolution_clock::now(); + PosCache posCache; +}; + +FrameInfo SampleStack::getPrimOpFrameInfo(const PrimOp & primOp, std::span args, PosIdx pos) +{ + auto derivationInfo = [&]() -> std::optional { + /* Here we rely a bit on the implementation details of libexpr/primops/derivation.nix + and derivationStrict primop. This is not ideal, but is necessary for + the usefulness of the profiler. This might actually affect the evaluation, + but the cost shouldn't be that high as to make the traces entirely inaccurate. */ + if (primOp.name == "derivationStrict") { + try { + /* Error context strings don't actually matter, since we ignore all eval errors. */ + state.forceAttrs(*args[0], pos, ""); + auto attrs = args[0]->attrs(); + auto nameAttr = state.getAttr(state.sName, attrs, ""); + auto drvName = std::string(state.forceStringNoCtx(*nameAttr->value, pos, "")); + return DerivationStrictFrameInfo{.callPos = pos, .drvName = std::move(drvName)}; + } catch (...) { + /* Ignore all errors, since those will be diagnosed by the evaluator itself. */ + } + } + + return std::nullopt; + }(); + + return derivationInfo.value_or(PrimOpFrameInfo{.expr = &primOp, .callPos = pos}); +} + +FrameInfo SampleStack::getFrameInfoFromValueAndPos(const Value & v, std::span args, PosIdx pos) +{ + /* NOTE: No actual references to garbage collected values are not held in + the profiler. */ + if (v.isLambda()) + return LambdaFrameInfo{.expr = v.lambda().fun, .callPos = pos}; + else if (v.isPrimOp()) { + return getPrimOpFrameInfo(*v.primOp(), args, pos); + } else if (v.isPrimOpApp()) + /* Resolve primOp eagerly. Must not hold on to a reference to a Value. */ + return PrimOpFrameInfo{.expr = v.primOpAppPrimOp(), .callPos = pos}; + else if (state.isFunctor(v)) { + const auto functor = v.attrs()->get(state.sFunctor); + if (auto pos_ = posCache.lookup(pos); std::holds_alternative(pos_.origin)) + /* HACK: In case callsite position is unresolved. */ + return FunctorFrameInfo{.pos = functor->pos}; + return FunctorFrameInfo{.pos = pos}; + } else + /* NOTE: Add a stack frame even for invalid cases (e.g. when calling a non-function). This is what + * trace-function-calls does. */ + return GenericFrameInfo{.pos = pos}; +} + +[[gnu::noinline]] void +SampleStack::preFunctionCallHook(EvalState & state, const Value & v, std::span args, const PosIdx pos) +{ + stack.push_back(getFrameInfoFromValueAndPos(v, args, pos)); + + auto now = std::chrono::high_resolution_clock::now(); + + if (now - lastStackSample > sampleInterval) { + callCount[stack] += 1; + lastStackSample = now; + } + + /* Do this in preFunctionCallHook because we might throw an exception, but + callFunction uses Finally, which doesn't play well with exceptions. */ + maybeSaveProfile(now); +} + +[[gnu::noinline]] void +SampleStack::postFunctionCallHook(EvalState & state, const Value & v, std::span args, const PosIdx pos) +{ + if (!stack.empty()) + stack.pop_back(); +} + +std::ostream & LambdaFrameInfo::symbolize(const EvalState & state, std::ostream & os, PosCache & posCache) const +{ + if (auto pos = posCache.lookup(callPos); std::holds_alternative(pos.origin)) + /* HACK: To avoid dubious «none»:0 in the generated profile if the origin can't be resolved + resort to printing the lambda location instead of the callsite position. */ + os << posCache.lookup(expr->getPos()); + else + os << pos; + if (expr->name) + os << ":" << state.symbols[expr->name]; + return os; +} + +std::ostream & GenericFrameInfo::symbolize(const EvalState & state, std::ostream & os, PosCache & posCache) const +{ + os << posCache.lookup(pos); + return os; +} + +std::ostream & FunctorFrameInfo::symbolize(const EvalState & state, std::ostream & os, PosCache & posCache) const +{ + os << posCache.lookup(pos) << ":functor"; + return os; +} + +std::ostream & PrimOpFrameInfo::symbolize(const EvalState & state, std::ostream & os, PosCache & posCache) const +{ + /* Sometimes callsite position can have an unresolved origin, which + leads to confusing «none»:0 locations in the profile. */ + auto pos = posCache.lookup(callPos); + if (!std::holds_alternative(pos.origin)) + os << posCache.lookup(callPos) << ":"; + os << *expr; + return os; +} + +std::ostream & +DerivationStrictFrameInfo::symbolize(const EvalState & state, std::ostream & os, PosCache & posCache) const +{ + /* Sometimes callsite position can have an unresolved origin, which + leads to confusing «none»:0 locations in the profile. */ + auto pos = posCache.lookup(callPos); + if (!std::holds_alternative(pos.origin)) + os << posCache.lookup(callPos) << ":"; + os << "primop derivationStrict:" << drvName; + return os; +} + +void SampleStack::maybeSaveProfile(std::chrono::time_point now) +{ + if (now - lastDump >= profileDumpInterval) + saveProfile(); + else + return; + + /* Save the last dump timepoint. Do this after actually saving data to file + to not account for the time doing the flushing to disk. */ + lastDump = std::chrono::high_resolution_clock::now(); + + /* Free up memory used for stack sampling. This might be very significant for + long-running evaluations, so we shouldn't hog too much memory. */ + callCount.clear(); +} + +void SampleStack::saveProfile() +{ + auto os = std::ostringstream{}; + for (auto & [stack, count] : callCount) { + auto first = true; + for (auto & pos : stack) { + if (first) + first = false; + else + os << ";"; + + std::visit([&](auto && info) { info.symbolize(state, os, posCache); }, pos); + } + os << " " << count; + writeLine(profileFd.get(), std::move(os).str()); + /* Clear ostringstream. */ + os.str(""); + os.clear(); + } +} + +SampleStack::~SampleStack() +{ + /* Guard against cases when we are already unwinding the stack. */ + try { + saveProfile(); + } catch (...) { + ignoreExceptionInDestructor(); + } +} + +} // namespace + +ref makeSampleStackProfiler(EvalState & state, std::filesystem::path profileFile, uint64_t frequency) +{ + /* 0 is a special value for sampling stack after each call. */ + std::chrono::nanoseconds period = frequency == 0 + ? std::chrono::nanoseconds{0} + : std::chrono::nanoseconds{std::nano::den / frequency / std::nano::num}; + return make_ref(state, profileFile, period); +} + +} diff --git a/src/libexpr/eval-settings.cc b/src/libexpr/eval-settings.cc index 4cbcb39b9e0..659c01a9e63 100644 --- a/src/libexpr/eval-settings.cc +++ b/src/libexpr/eval-settings.cc @@ -1,8 +1,8 @@ -#include "users.hh" -#include "globals.hh" -#include "profiles.hh" -#include "eval.hh" -#include "eval-settings.hh" +#include "nix/util/users.hh" +#include "nix/store/globals.hh" +#include "nix/store/profiles.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/eval-settings.hh" namespace nix { @@ -57,7 +57,7 @@ Strings EvalSettings::getDefaultNixPath() { Strings res; auto add = [&](const Path & p, const std::string & s = std::string()) { - if (pathAccessible(p)) { + if (std::filesystem::exists(p)) { if (s.empty()) { res.push_back(p); } else { @@ -103,4 +103,4 @@ Path getNixDefExpr() : getHome() + "/.nix-defexpr"; } -} +} // namespace nix \ No newline at end of file diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 05f58957ee3..47cc35daa8c 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -1,24 +1,27 @@ -#include "eval.hh" -#include "eval-settings.hh" -#include "primops.hh" -#include "print-options.hh" -#include "exit.hh" -#include "types.hh" -#include "util.hh" -#include "store-api.hh" -#include "derivations.hh" -#include "downstream-placeholder.hh" -#include "eval-inline.hh" -#include "filetransfer.hh" -#include "function-trace.hh" -#include "profiles.hh" -#include "print.hh" -#include "filtering-source-accessor.hh" -#include "memory-source-accessor.hh" -#include "gc-small-vector.hh" -#include "url.hh" -#include "fetch-to-store.hh" -#include "tarball.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/eval-settings.hh" +#include "nix/expr/primops.hh" +#include "nix/expr/print-options.hh" +#include "nix/expr/symbol-table.hh" +#include "nix/util/exit.hh" +#include "nix/util/types.hh" +#include "nix/util/util.hh" +#include "nix/store/store-api.hh" +#include "nix/store/derivations.hh" +#include "nix/store/downstream-placeholder.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/store/filetransfer.hh" +#include "nix/expr/function-trace.hh" +#include "nix/store/profiles.hh" +#include "nix/expr/print.hh" +#include "nix/fetchers/filtering-source-accessor.hh" +#include "nix/util/memory-source-accessor.hh" +#include "nix/expr/gc-small-vector.hh" +#include "nix/util/url.hh" +#include "nix/fetchers/fetch-to-store.hh" +#include "nix/fetchers/tarball.hh" +#include "nix/fetchers/input-cache.hh" + #include "parser-tab.hh" #include @@ -38,7 +41,7 @@ # include #endif -#include "strings-inline.hh" +#include "nix/util/strings-inline.hh" using json = nlohmann::json; @@ -87,18 +90,14 @@ std::string printValue(EvalState & state, Value & v) return out.str(); } -void Value::print(EvalState & state, std::ostream & str, PrintOptions options) +Value * Value::toPtr(SymbolStr str) noexcept { - printValue(state, str, *this, options); + return const_cast(str.valuePtr()); } -const Value * getPrimOp(const Value &v) { - const Value * primOp = &v; - while (primOp->isPrimOpApp()) { - primOp = primOp->payload.primOpApp.left; - } - assert(primOp->isPrimOp()); - return primOp; +void Value::print(EvalState & state, std::ostream & str, PrintOptions options) +{ + printValue(state, str, *this, options); } std::string_view showType(ValueType type, bool withArticle) @@ -126,12 +125,12 @@ std::string showType(const Value & v) // Allow selecting a subset of enum values #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wswitch-enum" - switch (v.internalType) { - case tString: return v.payload.string.context ? "a string with context" : "a string"; + switch (v.getInternalType()) { + case tString: return v.context() ? "a string with context" : "a string"; case tPrimOp: - return fmt("the built-in function '%s'", std::string(v.payload.primOp->name)); + return fmt("the built-in function '%s'", std::string(v.primOp()->name)); case tPrimOpApp: - return fmt("the partially applied built-in function '%s'", std::string(getPrimOp(v)->payload.primOp->name)); + return fmt("the partially applied built-in function '%s'", v.primOpAppPrimOp()->name); case tExternal: return v.external()->showType(); case tThunk: return v.isBlackhole() ? "a black hole" : "a thunk"; case tApp: return "a function application"; @@ -146,10 +145,10 @@ PosIdx Value::determinePos(const PosIdx pos) const // Allow selecting a subset of enum values #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wswitch-enum" - switch (internalType) { + switch (getInternalType()) { case tAttrs: return attrs()->pos; - case tLambda: return payload.lambda.fun->pos; - case tApp: return payload.app.left->determinePos(pos); + case tLambda: return lambda().fun->pos; + case tApp: return app().left->determinePos(pos); default: return pos; } #pragma GCC diagnostic pop @@ -158,13 +157,12 @@ PosIdx Value::determinePos(const PosIdx pos) const bool Value::isTrivial() const { return - internalType != tApp - && internalType != tPrimOpApp - && (internalType != tThunk - || (dynamic_cast(payload.thunk.expr) - && ((ExprAttrs *) payload.thunk.expr)->dynamicAttrs.empty()) - || dynamic_cast(payload.thunk.expr) - || dynamic_cast(payload.thunk.expr)); + !isa() + && (!isa() + || (dynamic_cast(thunk().expr) + && ((ExprAttrs *) thunk().expr)->dynamicAttrs.empty()) + || dynamic_cast(thunk().expr) + || dynamic_cast(thunk().expr)); } @@ -210,6 +208,7 @@ EvalState::EvalState( , sRight(symbols.create("right")) , sWrong(symbols.create("wrong")) , sStructuredAttrs(symbols.create("__structuredAttrs")) + , sJson(symbols.create("__json")) , sAllowedReferences(symbols.create("allowedReferences")) , sAllowedRequisites(symbols.create("allowedRequisites")) , sDisallowedReferences(symbols.create("disallowedReferences")) @@ -245,33 +244,76 @@ EvalState::EvalState( } , repair(NoRepair) , emptyBindings(0) - , rootFS( - settings.restrictEval || settings.pureEval - ? ref(AllowListSourceAccessor::create(getFSSourceAccessor(), {}, - [&settings](const CanonPath & path) -> RestrictedPathError { - auto modeInformation = settings.pureEval - ? "in pure evaluation mode (use '--impure' to override)" - : "in restricted mode"; - throw RestrictedPathError("access to absolute path '%1%' is forbidden %2%", path, modeInformation); + , storeFS( + makeMountedSourceAccessor( + { + {CanonPath::root, makeEmptySourceAccessor()}, + /* In the pure eval case, we can simply require + valid paths. However, in the *impure* eval + case this gets in the way of the union + mechanism, because an invalid access in the + upper layer will *not* be caught by the union + source accessor, but instead abort the entire + lookup. + + This happens when the store dir in the + ambient file system has a path (e.g. because + another Nix store there), but the relocated + store does not. + + TODO make the various source accessors doing + access control all throw the same type of + exception, and make union source accessor + catch it, so we don't need to do this hack. + */ + {CanonPath(store->storeDir), store->getFSAccessor(settings.pureEval)}, })) - : getFSSourceAccessor()) + , rootFS( + ({ + /* In pure eval mode, we provide a filesystem that only + contains the Nix store. + + If we have a chroot store and pure eval is not enabled, + use a union accessor to make the chroot store available + at its logical location while still having the + underlying directory available. This is necessary for + instance if we're evaluating a file from the physical + /nix/store while using a chroot store. */ + auto accessor = getFSSourceAccessor(); + + auto realStoreDir = dirOf(store->toRealPath(StorePath::dummy)); + if (settings.pureEval || store->storeDir != realStoreDir) { + accessor = settings.pureEval + ? storeFS + : makeUnionSourceAccessor({accessor, storeFS}); + } + + /* Apply access control if needed. */ + if (settings.restrictEval || settings.pureEval) + accessor = AllowListSourceAccessor::create(accessor, {}, {}, + [&settings](const CanonPath & path) -> RestrictedPathError { + auto modeInformation = settings.pureEval + ? "in pure evaluation mode (use '--impure' to override)" + : "in restricted mode"; + throw RestrictedPathError("access to absolute path '%1%' is forbidden %2%", path, modeInformation); + }); + + accessor; + })) , corepkgsFS(make_ref()) , internalFS(make_ref()) , derivationInternal{corepkgsFS->addFile( CanonPath("derivation-internal.nix"), #include "primops/derivation.nix.gen.hh" )} - , callFlakeInternal{internalFS->addFile( - CanonPath("call-flake.nix"), - #include "call-flake.nix.gen.hh" - )} , store(store) , buildStore(buildStore ? buildStore : store) + , inputCache(fetchers::InputCache::create()) , debugRepl(nullptr) , debugStop(false) , trylevel(0) , regexCache(makeRegexCache()) -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC , valueAllocCache(std::allocate_shared(traceable_allocator(), nullptr)) , env1AllocCache(std::allocate_shared(traceable_allocator(), nullptr)) , baseEnvP(std::allocate_shared(traceable_allocator(), &allocEnv(BASE_ENV_SIZE))) @@ -326,9 +368,21 @@ EvalState::EvalState( #include "fetchurl.nix.gen.hh" ); - createBaseEnv(); -} + createBaseEnv(settings); + + /* Register function call tracer. */ + if (settings.traceFunctionCalls) + profiler.addProfiler(make_ref()); + switch (settings.evalProfilerMode) { + case EvalProfilerMode::flamegraph: + profiler.addProfiler(makeSampleStackProfiler( + *this, settings.evalProfileFile.get(), settings.evalProfilerFrequency)); + break; + case EvalProfilerMode::disabled: + break; + } +} EvalState::~EvalState() { @@ -344,7 +398,17 @@ void EvalState::allowPath(const Path & path) void EvalState::allowPath(const StorePath & storePath) { if (auto rootFS2 = rootFS.dynamic_pointer_cast()) - rootFS2->allowPrefix(CanonPath(store->toRealPath(storePath))); + rootFS2->allowPrefix(CanonPath(store->printStorePath(storePath))); +} + +void EvalState::allowClosure(const StorePath & storePath) +{ + if (!rootFS.dynamic_pointer_cast()) return; + + StorePathSet closure; + store->computeFSClosure(storePath, closure); + for (auto & p : closure) + allowPath(p); } void EvalState::allowAndSetStorePathString(const StorePath & storePath, Value & v) @@ -396,7 +460,7 @@ void EvalState::checkURI(const std::string & uri) /* If the URI is a path, then check it against allowedPaths as well. */ - if (hasPrefix(uri, "/")) { + if (isAbsolute(uri)) { if (auto rootFS2 = rootFS.dynamic_pointer_cast()) rootFS2->checkAccess(CanonPath(uri)); return; @@ -412,16 +476,6 @@ void EvalState::checkURI(const std::string & uri) } -Path EvalState::toRealPath(const Path & path, const NixStringContext & context) -{ - // FIXME: check whether 'path' is in 'context'. - return - !context.empty() && store->isInStore(path) - ? store->toRealPath(path) - : path; -} - - Value * EvalState::addConstant(const std::string & name, Value & v, Constant info) { Value * v2 = allocValue(); @@ -448,7 +502,7 @@ void EvalState::addConstant(const std::string & name, Value * v, Constant info) /* Install value the base environment. */ staticBaseEnv->vars.emplace_back(symbols.create(name), baseEnvDispl); baseEnv.values[baseEnvDispl++] = v; - getBuiltins().payload.attrs->push_back(Attr(symbols.create(name2), v)); + const_cast(getBuiltins().attrs())->push_back(Attr(symbols.create(name2), v)); } } @@ -470,13 +524,15 @@ std::ostream & operator<<(std::ostream & output, const PrimOp & primOp) const PrimOp * Value::primOpAppPrimOp() const { - Value * left = payload.primOpApp.left; + Value * left = primOpApp().left; while (left && !left->isPrimOp()) { - left = left->payload.primOpApp.left; + left = left->primOpApp().left; } if (!left) return nullptr; + + assert(left->isPrimOp()); return left->primOp(); } @@ -484,7 +540,7 @@ const PrimOp * Value::primOpAppPrimOp() const void Value::mkPrimOp(PrimOp * p) { p->check(); - finishValue(tPrimOp, { .primOp = p }); + setStorage(p); } @@ -516,7 +572,7 @@ Value * EvalState::addPrimOp(PrimOp && primOp) else { staticBaseEnv->vars.emplace_back(envName, baseEnvDispl); baseEnv.values[baseEnvDispl++] = v; - getBuiltins().payload.attrs->push_back(Attr(symbols.create(primOp.name), v)); + const_cast(getBuiltins().attrs())->push_back(Attr(symbols.create(primOp.name), v)); } return v; @@ -553,7 +609,7 @@ std::optional EvalState::getDoc(Value & v) }; } if (v.isLambda()) { - auto exprLambda = v.payload.lambda.fun; + auto exprLambda = v.lambda().fun; std::ostringstream s; std::string name; @@ -600,7 +656,7 @@ std::optional EvalState::getDoc(Value & v) Value & functor = *v.attrs()->find(sFunctor)->value; Value * vp[] = {&v}; Value partiallyApplied; - // The first paramater is not user-provided, and may be + // The first parameter is not user-provided, and may be // handled by code that is opaque to the user, like lib.const = x: y: y; // So preferably we show docs that are relevant to the // "partially applied" function returned by e.g. `const`. @@ -744,18 +800,26 @@ void EvalState::runDebugRepl(const Error * error, const Env & env, const Expr & if (!debugRepl || inDebugger) return; - auto dts = - error && expr.getPos() - ? std::make_unique( - *this, - DebugTrace { - .pos = error->info().pos ? error->info().pos : positions[expr.getPos()], + auto dts = [&]() -> std::unique_ptr { + if (error && expr.getPos()) { + auto trace = DebugTrace{ + .pos = [&]() -> std::variant { + if (error->info().pos) { + if (auto * pos = error->info().pos.get()) + return *pos; + return noPos; + } + return expr.getPos(); + }(), .expr = expr, .env = env, .hint = error->info().msg, - .isError = true - }) - : nullptr; + .isError = true}; + + return std::make_unique(*this, std::move(trace)); + } + return nullptr; + }(); if (error) { @@ -800,7 +864,7 @@ static std::unique_ptr makeDebugTraceStacker( EvalState & state, Expr & expr, Env & env, - std::shared_ptr && pos, + std::variant pos, const Args & ... formatArgs) { return std::make_unique(state, @@ -1077,7 +1141,7 @@ void EvalState::evalFile(const SourcePath & path, Value & v, bool mustBeTrivial) *this, *e, this->baseEnv, - e->getPos() ? std::make_shared(positions[e->getPos()]) : nullptr, + e->getPos(), "while evaluating the file '%1%':", resolvedPath.to_string()) : nullptr; @@ -1101,6 +1165,7 @@ void EvalState::resetFileCache() { fileEvalCache.clear(); fileParseCache.clear(); + inputCache->clear(); } @@ -1303,9 +1368,7 @@ void ExprLet::eval(EvalState & state, Env & env, Value & v) state, *this, env2, - getPos() - ? std::make_shared(state.positions[getPos()]) - : nullptr, + getPos(), "while evaluating a '%1%' expression", "let" ) @@ -1374,7 +1437,7 @@ void ExprSelect::eval(EvalState & state, Env & env, Value & v) state, *this, env, - state.positions[getPos()], + getPos(), "while evaluating the attribute '%1%'", showAttrPath(state, env, attrPath)) : nullptr; @@ -1394,7 +1457,7 @@ void ExprSelect::eval(EvalState & state, Env & env, Value & v) } else { state.forceAttrs(*vAttrs, pos, "while selecting an attribute"); if (!(j = vAttrs->attrs()->get(name))) { - std::set allAttrNames; + StringSet allAttrNames; for (auto & attr : *vAttrs->attrs()) allAttrNames.insert(std::string(state.symbols[attr.name])); auto suggestions = Suggestions::bestMatches(allAttrNames, state.symbols[name]); @@ -1474,9 +1537,14 @@ void EvalState::callFunction(Value & fun, std::span args, Value & vRes, { auto _level = addCallDepth(pos); - auto trace = settings.traceFunctionCalls - ? std::make_unique(positions[pos]) - : nullptr; + auto neededHooks = profiler.getNeededHooks(); + if (neededHooks.test(EvalProfiler::preFunctionCall)) [[unlikely]] + profiler.preFunctionCallHook(*this, fun, args, pos); + + Finally traceExit_{[&](){ + if (profiler.getNeededHooks().test(EvalProfiler::postFunctionCall)) [[unlikely]] + profiler.postFunctionCallHook(*this, fun, args, pos); + }}; forceValue(fun, pos); @@ -1498,13 +1566,13 @@ void EvalState::callFunction(Value & fun, std::span args, Value & vRes, if (vCur.isLambda()) { - ExprLambda & lambda(*vCur.payload.lambda.fun); + ExprLambda & lambda(*vCur.lambda().fun); auto size = (!lambda.arg ? 0 : 1) + (lambda.hasFormals() ? lambda.formals->formals.size() : 0); Env & env2(allocEnv(size)); - env2.up = vCur.payload.lambda.env; + env2.up = vCur.lambda().env; Displacement displ = 0; @@ -1534,7 +1602,7 @@ void EvalState::callFunction(Value & fun, std::span args, Value & vRes, symbols[i.name]) .atPos(lambda.pos) .withTrace(pos, "from call site") - .withFrame(*fun.payload.lambda.env, lambda) + .withFrame(*vCur.lambda().env, lambda) .debugThrow(); } env2.values[displ++] = i.def->maybeThunk(*this, env2); @@ -1551,7 +1619,7 @@ void EvalState::callFunction(Value & fun, std::span args, Value & vRes, user. */ for (auto & i : *args[0]->attrs()) if (!lambda.formals->has(i.name)) { - std::set formalNames; + StringSet formalNames; for (auto & formal : lambda.formals->formals) formalNames.insert(std::string(symbols[formal.name])); auto suggestions = Suggestions::bestMatches(formalNames, symbols[i.name]); @@ -1561,7 +1629,7 @@ void EvalState::callFunction(Value & fun, std::span args, Value & vRes, .atPos(lambda.pos) .withTrace(pos, "from call site") .withSuggestions(suggestions) - .withFrame(*fun.payload.lambda.env, lambda) + .withFrame(*vCur.lambda().env, lambda) .debugThrow(); } unreachable(); @@ -1575,7 +1643,7 @@ void EvalState::callFunction(Value & fun, std::span args, Value & vRes, try { auto dts = debugRepl ? makeDebugTraceStacker( - *this, *lambda.body, env2, positions[lambda.pos], + *this, *lambda.body, env2, lambda.pos, "while calling %s", lambda.name ? concatStrings("'", symbols[lambda.name], "'") @@ -1633,7 +1701,7 @@ void EvalState::callFunction(Value & fun, std::span args, Value & vRes, Value * primOp = &vCur; while (primOp->isPrimOpApp()) { argsDone++; - primOp = primOp->payload.primOpApp.left; + primOp = primOp->primOpApp().left; } assert(primOp->isPrimOp()); auto arity = primOp->primOp()->arity; @@ -1649,8 +1717,8 @@ void EvalState::callFunction(Value & fun, std::span args, Value & vRes, Value * vArgs[maxPrimOpArity]; auto n = argsDone; - for (Value * arg = &vCur; arg->isPrimOpApp(); arg = arg->payload.primOpApp.left) - vArgs[--n] = arg->payload.primOpApp.right; + for (Value * arg = &vCur; arg->isPrimOpApp(); arg = arg->primOpApp().left) + vArgs[--n] = arg->primOpApp().right; for (size_t i = 0; i < argsLeft; ++i) vArgs[argsDone + i] = args[i]; @@ -1710,9 +1778,7 @@ void ExprCall::eval(EvalState & state, Env & env, Value & v) state, *this, env, - getPos() - ? std::make_shared(state.positions[getPos()]) - : nullptr, + getPos(), "while calling a function" ) : nullptr; @@ -1758,14 +1824,14 @@ void EvalState::autoCallFunction(const Bindings & args, Value & fun, Value & res } } - if (!fun.isLambda() || !fun.payload.lambda.fun->hasFormals()) { + if (!fun.isLambda() || !fun.lambda().fun->hasFormals()) { res = fun; return; } - auto attrs = buildBindings(std::max(static_cast(fun.payload.lambda.fun->formals->formals.size()), args.size())); + auto attrs = buildBindings(std::max(static_cast(fun.lambda().fun->formals->formals.size()), args.size())); - if (fun.payload.lambda.fun->formals->ellipsis) { + if (fun.lambda().fun->formals->ellipsis) { // If the formals have an ellipsis (eg the function accepts extra args) pass // all available automatic arguments (which includes arguments specified on // the command line via --arg/--argstr) @@ -1773,7 +1839,7 @@ void EvalState::autoCallFunction(const Bindings & args, Value & fun, Value & res attrs.insert(v); } else { // Otherwise, only pass the arguments that the function accepts - for (auto & i : fun.payload.lambda.fun->formals->formals) { + for (auto & i : fun.lambda().fun->formals->formals) { auto j = args.get(i.name); if (j) { attrs.insert(*j); @@ -1783,7 +1849,7 @@ Nix attempted to evaluate a function as a top level expression; in this case it must have its arguments supplied either by default values, or passed explicitly with '--arg' or '--argstr'. See https://nixos.org/manual/nix/stable/language/constructs.html#functions.)", symbols[i.name]) - .atPos(i.pos).withFrame(*fun.payload.lambda.env, *fun.payload.lambda.fun).debugThrow(); + .atPos(i.pos).withFrame(*fun.lambda().env, *fun.lambda().fun).debugThrow(); } } } @@ -1941,9 +2007,10 @@ void EvalState::concatLists(Value & v, size_t nrLists, Value * const * lists, co auto list = buildList(len); auto out = list.elems; for (size_t n = 0, pos = 0; n < nrLists; ++n) { - auto l = lists[n]->listSize(); + auto listView = lists[n]->listView(); + auto l = listView.size(); if (l) - memcpy(out + pos, lists[n]->listElems(), l * sizeof(Value *)); + memcpy(out + pos, listView.data(), l * sizeof(Value *)); pos += l; } v.mkList(list); @@ -2041,7 +2108,7 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v) else if (firstType == nPath) { if (!context.empty()) state.error("a string that refers to a store path cannot be appended to a path").atPos(pos).withFrame(env, *this).debugThrow(); - v.mkPath(state.rootPath(CanonPath(canonPath(str())))); + v.mkPath(state.rootPath(CanonPath(str()))); } else v.mkStringMove(c_str(), context); } @@ -2096,7 +2163,7 @@ void EvalState::forceValueDeep(Value & v) try { // If the value is a thunk, we're evaling. Otherwise no trace necessary. auto dts = debugRepl && i.value->isThunk() - ? makeDebugTraceStacker(*this, *i.value->payload.thunk.expr, *i.value->payload.thunk.env, positions[i.pos], + ? makeDebugTraceStacker(*this, *i.value->thunk().expr, *i.value->thunk().env, i.pos, "while evaluating the attribute '%1%'", symbols[i.name]) : nullptr; @@ -2108,7 +2175,7 @@ void EvalState::forceValueDeep(Value & v) } else if (v.isList()) { - for (auto v2 : v.listItems()) + for (auto v2 : v.listView()) recurse(*v2); } }; @@ -2176,8 +2243,18 @@ bool EvalState::forceBool(Value & v, const PosIdx pos, std::string_view errorCtx return v.boolean(); } +Bindings::const_iterator EvalState::getAttr(Symbol attrSym, const Bindings * attrSet, std::string_view errorCtx) +{ + auto value = attrSet->find(attrSym); + if (value == attrSet->end()) { + error("attribute '%s' missing", symbols[attrSym]) + .withTrace(noPos, errorCtx) + .debugThrow(); + } + return value; +} -bool EvalState::isFunctor(Value & fun) +bool EvalState::isFunctor(const Value & fun) const { return fun.type() == nAttrs && fun.attrs()->find(sFunctor) != fun.attrs()->end(); } @@ -2218,18 +2295,18 @@ std::string_view EvalState::forceString(Value & v, const PosIdx pos, std::string } -void copyContext(const Value & v, NixStringContext & context) +void copyContext(const Value & v, NixStringContext & context, const ExperimentalFeatureSettings & xpSettings) { - if (v.payload.string.context) - for (const char * * p = v.payload.string.context; *p; ++p) - context.insert(NixStringContextElem::parse(*p)); + if (v.context()) + for (const char * * p = v.context(); *p; ++p) + context.insert(NixStringContextElem::parse(*p, xpSettings)); } -std::string_view EvalState::forceString(Value & v, NixStringContext & context, const PosIdx pos, std::string_view errorCtx) +std::string_view EvalState::forceString(Value & v, NixStringContext & context, const PosIdx pos, std::string_view errorCtx, const ExperimentalFeatureSettings & xpSettings) { auto s = forceString(v, pos, errorCtx); - copyContext(v, context); + copyContext(v, context, xpSettings); return s; } @@ -2291,7 +2368,7 @@ BackedStringView EvalState::coerceToString( !canonicalizePath && !copyToStore ? // FIXME: hack to preserve path literals that end in a // slash, as in /foo/${x}. - v.payload.path.path + v.pathStr() : copyToStore ? store->printStorePath(copyPathToStore(context, v.path())) : std::string(v.path().path.abs()); @@ -2335,7 +2412,8 @@ BackedStringView EvalState::coerceToString( if (v.isList()) { std::string result; - for (auto [n, v2] : enumerate(v.listItems())) { + auto listView = v.listView(); + for (auto [n, v2] : enumerate(listView)) { try { result += *coerceToString(pos, *v2, context, "while evaluating one element of the list", @@ -2373,8 +2451,9 @@ StorePath EvalState::copyPathToStore(NixStringContext & context, const SourcePat ? *dstPathCached : [&]() { auto dstPath = fetchToStore( + fetchSettings, *store, - path.resolveSymlinks(), + path.resolveSymlinks(SymlinkResolution::Ancestors), settings.readOnlyMode ? FetchMode::DryRun : FetchMode::Copy, path.baseName(), ContentAddressMethod::Raw::NixArchive, @@ -2417,12 +2496,12 @@ SourcePath EvalState::coerceToPath(const PosIdx pos, Value & v, NixStringContext } } - /* Any other value should be coercable to a string, interpreted + /* Any other value should be coercible to a string, interpreted relative to the root filesystem. */ auto path = coerceToString(pos, v, context, errorCtx, false, false, true).toOwned(); if (path == "" || path[0] != '/') error("string '%1%' doesn't represent an absolute path", path).withTrace(pos, errorCtx).debugThrow(); - return rootPath(CanonPath(path)); + return rootPath(path); } @@ -2435,10 +2514,10 @@ StorePath EvalState::coerceToStorePath(const PosIdx pos, Value & v, NixStringCon } -std::pair EvalState::coerceToSingleDerivedPathUnchecked(const PosIdx pos, Value & v, std::string_view errorCtx) +std::pair EvalState::coerceToSingleDerivedPathUnchecked(const PosIdx pos, Value & v, std::string_view errorCtx, const ExperimentalFeatureSettings & xpSettings) { NixStringContext context; - auto s = forceString(v, context, pos, errorCtx); + auto s = forceString(v, context, pos, errorCtx, xpSettings); auto csize = context.size(); if (csize != 1) error( @@ -2558,14 +2637,14 @@ void EvalState::assertEqValues(Value & v1, Value & v2, const PosIdx pos, std::st return; case nPath: - if (v1.payload.path.accessor != v2.payload.path.accessor) { + if (v1.pathAccessor() != v2.pathAccessor()) { error( "path '%s' is not equal to path '%s' because their accessors are different", ValuePrinter(*this, v1, errorPrintOptions), ValuePrinter(*this, v2, errorPrintOptions)) .debugThrow(); } - if (strcmp(v1.payload.path.path, v2.payload.path.path) != 0) { + if (strcmp(v1.pathStr(), v2.pathStr()) != 0) { error( "path '%s' is not equal to path '%s'", ValuePrinter(*this, v1, errorPrintOptions), @@ -2589,7 +2668,7 @@ void EvalState::assertEqValues(Value & v1, Value & v2, const PosIdx pos, std::st } for (size_t n = 0; n < v1.listSize(); ++n) { try { - assertEqValues(*v1.listElems()[n], *v2.listElems()[n], pos, errorCtx); + assertEqValues(*v1.listView()[n], *v2.listView()[n], pos, errorCtx); } catch (Error & e) { e.addTrace(positions[pos], "while comparing list element %d", n); throw; @@ -2732,8 +2811,8 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v case nPath: return // FIXME: compare accessors by their fingerprint. - v1.payload.path.accessor == v2.payload.path.accessor - && strcmp(v1.payload.path.path, v2.payload.path.path) == 0; + v1.pathAccessor() == v2.pathAccessor() + && strcmp(v1.pathStr(), v2.pathStr()) == 0; case nNull: return true; @@ -2741,7 +2820,7 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v case nList: if (v1.listSize() != v2.listSize()) return false; for (size_t n = 0; n < v1.listSize(); ++n) - if (!eqValues(*v1.listElems()[n], *v2.listElems()[n], pos, errorCtx)) return false; + if (!eqValues(*v1.listView()[n], *v2.listView()[n], pos, errorCtx)) return false; return true; case nAttrs: { @@ -2784,11 +2863,11 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v } bool EvalState::fullGC() { -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC GC_gcollect(); // Check that it ran. We might replace this with a version that uses more // of the boehm API to get this reliably, at a maintenance cost. - // We use a 1K margin because technically this has a race condtion, but we + // We use a 1K margin because technically this has a race condition, but we // probably won't encounter it in practice, because the CLI isn't concurrent // like that. return GC_get_bytes_since_gc() < 1024; @@ -2803,7 +2882,7 @@ void EvalState::maybePrintStats() if (showStats) { // Make the final heap size more deterministic. -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC if (!fullGC()) { warn("failed to perform a full GC before reporting stats"); } @@ -2825,7 +2904,7 @@ void EvalState::printStatistics() uint64_t bValues = nrValues * sizeof(Value); uint64_t bAttrsets = nrAttrsets * sizeof(Bindings) + nrAttrsInAttrsets * sizeof(Attr); -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC GC_word heapSize, totalBytes; GC_get_heap_usage_safe(&heapSize, 0, 0, 0, &totalBytes); double gcFullOnlyTime = ({ @@ -2847,7 +2926,7 @@ void EvalState::printStatistics() #ifndef _WIN32 // TODO implement {"cpu", cpuTime}, #endif -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC {GC_is_incremental_mode() ? "gcNonIncremental" : "gc", gcFullOnlyTime}, #ifndef _WIN32 // TODO implement {GC_is_incremental_mode() ? "gcNonIncrementalFraction" : "gcFraction", gcFullOnlyTime / cpuTime}, @@ -2891,7 +2970,7 @@ void EvalState::printStatistics() topObj["nrLookups"] = nrLookups; topObj["nrPrimOpCalls"] = nrPrimOpCalls; topObj["nrFunctionCalls"] = nrFunctionCalls; -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC topObj["gc"] = { {"heapSize", heapSize}, {"totalBytes", totalBytes}, @@ -2941,7 +3020,7 @@ void EvalState::printStatistics() // XXX: overrides earlier assignment topObj["symbols"] = json::array(); auto &list = topObj["symbols"]; - symbols.dump([&](const std::string & s) { list.emplace_back(s); }); + symbols.dump([&](std::string_view s) { list.emplace_back(s); }); } if (outPath == "-") { std::cerr << topObj.dump(2) << std::endl; @@ -3060,8 +3139,11 @@ std::optional EvalState::resolveLookupPathPath(const LookupPath::Pat auto i = lookupPathResolved.find(value); if (i != lookupPathResolved.end()) return i->second; - auto finish = [&](SourcePath res) { - debug("resolved search path element '%s' to '%s'", value, res); + auto finish = [&](std::optional res) { + if (res) + debug("resolved search path element '%s' to '%s'", value, *res); + else + debug("failed to resolve search path element '%s'", value); lookupPathResolved.emplace(value, res); return res; }; @@ -3072,8 +3154,8 @@ std::optional EvalState::resolveLookupPathPath(const LookupPath::Pat store, fetchSettings, EvalSettings::resolvePseudoUrl(value)); - auto storePath = fetchToStore(*store, SourcePath(accessor), FetchMode::Copy); - return finish(rootPath(store->toRealPath(storePath))); + auto storePath = fetchToStore(fetchSettings, *store, SourcePath(accessor), FetchMode::Copy); + return finish(this->storePath(storePath)); } catch (Error & e) { logWarning({ .msg = HintFmt("Nix search path entry '%1%' cannot be downloaded, ignoring", value) @@ -3099,15 +3181,12 @@ std::optional EvalState::resolveLookupPathPath(const LookupPath::Pat allowPath(path.path.abs()); if (store->isInStore(path.path.abs())) { try { - StorePathSet closure; - store->computeFSClosure(store->toStorePath(path.path.abs()).first, closure); - for (auto & p : closure) - allowPath(p); + allowClosure(store->toStorePath(path.path.abs()).first); } catch (InvalidPath &) { } } } - if (path.pathExists()) + if (path.resolveSymlinks().pathExists()) return finish(std::move(path)); else { logWarning({ @@ -3116,8 +3195,7 @@ std::optional EvalState::resolveLookupPathPath(const LookupPath::Pat } } - debug("failed to resolve search path element '%s'", value); - return std::nullopt; + return finish(std::nullopt); } @@ -3178,5 +3256,18 @@ std::ostream & operator << (std::ostream & str, const ExternalValueBase & v) { return v.print(str); } +void forceNoNullByte(std::string_view s, std::function pos) +{ + if (s.find('\0') != s.npos) { + using namespace std::string_view_literals; + auto str = replaceStrings(std::string(s), "\0"sv, "â€"sv); + Error error("input string '%s' cannot be represented as Nix string because it contains null bytes", str); + if (pos) { + error.atPos(pos()); + } + throw error; + } +} + } diff --git a/src/libexpr/fetchurl.nix b/src/libexpr/fetchurl.nix index 85a01d16179..72b3b00dffc 100644 --- a/src/libexpr/fetchurl.nix +++ b/src/libexpr/fetchurl.nix @@ -1,40 +1,72 @@ -{ system ? "" # obsolete -, url -, hash ? "" # an SRI hash - -# Legacy hash specification -, md5 ? "", sha1 ? "", sha256 ? "", sha512 ? "" -, outputHash ? - if hash != "" then hash else if sha512 != "" then sha512 else if sha1 != "" then sha1 else if md5 != "" then md5 else sha256 -, outputHashAlgo ? - if hash != "" then "" else if sha512 != "" then "sha512" else if sha1 != "" then "sha1" else if md5 != "" then "md5" else "sha256" - -, executable ? false -, unpack ? false -, name ? baseNameOf (toString url) -, impure ? false +{ + system ? "", # obsolete + url, + hash ? "", # an SRI hash + + # Legacy hash specification + md5 ? "", + sha1 ? "", + sha256 ? "", + sha512 ? "", + outputHash ? + if hash != "" then + hash + else if sha512 != "" then + sha512 + else if sha1 != "" then + sha1 + else if md5 != "" then + md5 + else + sha256, + outputHashAlgo ? + if hash != "" then + "" + else if sha512 != "" then + "sha512" + else if sha1 != "" then + "sha1" + else if md5 != "" then + "md5" + else + "sha256", + + executable ? false, + unpack ? false, + name ? baseNameOf (toString url), + impure ? false, }: -derivation ({ - builder = "builtin:fetchurl"; +derivation ( + { + builder = "builtin:fetchurl"; - # New-style output content requirements. - outputHashMode = if unpack || executable then "recursive" else "flat"; + # New-style output content requirements. + outputHashMode = if unpack || executable then "recursive" else "flat"; - inherit name url executable unpack; + inherit + name + url + executable + unpack + ; - system = "builtin"; + system = "builtin"; - # No need to double the amount of network traffic - preferLocalBuild = true; + # No need to double the amount of network traffic + preferLocalBuild = true; - # This attribute does nothing; it's here to avoid changing evaluation results. - impureEnvVars = [ - "http_proxy" "https_proxy" "ftp_proxy" "all_proxy" "no_proxy" - ]; + # This attribute does nothing; it's here to avoid changing evaluation results. + impureEnvVars = [ + "http_proxy" + "https_proxy" + "ftp_proxy" + "all_proxy" + "no_proxy" + ]; - # To make "nix-prefetch-url" work. - urls = [ url ]; -} // (if impure - then { __impure = true; } - else { inherit outputHashAlgo outputHash; })) + # To make "nix-prefetch-url" work. + urls = [ url ]; + } + // (if impure then { __impure = true; } else { inherit outputHashAlgo outputHash; }) +) diff --git a/src/libexpr/function-trace.cc b/src/libexpr/function-trace.cc index c6057b3842f..cda3bc2db41 100644 --- a/src/libexpr/function-trace.cc +++ b/src/libexpr/function-trace.cc @@ -1,18 +1,22 @@ -#include "function-trace.hh" -#include "logging.hh" +#include "nix/expr/function-trace.hh" +#include "nix/util/logging.hh" namespace nix { -FunctionCallTrace::FunctionCallTrace(const Pos & pos) : pos(pos) { +void FunctionCallTrace::preFunctionCallHook( + EvalState & state, const Value & v, std::span args, const PosIdx pos) +{ auto duration = std::chrono::high_resolution_clock::now().time_since_epoch(); auto ns = std::chrono::duration_cast(duration); - printMsg(lvlInfo, "function-trace entered %1% at %2%", pos, ns.count()); + printMsg(lvlInfo, "function-trace entered %1% at %2%", state.positions[pos], ns.count()); } -FunctionCallTrace::~FunctionCallTrace() { +void FunctionCallTrace::postFunctionCallHook( + EvalState & state, const Value & v, std::span args, const PosIdx pos) +{ auto duration = std::chrono::high_resolution_clock::now().time_since_epoch(); auto ns = std::chrono::duration_cast(duration); - printMsg(lvlInfo, "function-trace exited %1% at %2%", pos, ns.count()); + printMsg(lvlInfo, "function-trace exited %1% at %2%", state.positions[pos], ns.count()); } } diff --git a/src/libexpr/function-trace.hh b/src/libexpr/function-trace.hh deleted file mode 100644 index 91439b0aad2..00000000000 --- a/src/libexpr/function-trace.hh +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once -///@file - -#include "eval.hh" - -#include - -namespace nix { - -struct FunctionCallTrace -{ - const Pos pos; - FunctionCallTrace(const Pos & pos); - ~FunctionCallTrace(); -}; -} diff --git a/src/libexpr/get-drvs.cc b/src/libexpr/get-drvs.cc index 1ac13fcd2b1..3c9ff9ff3c6 100644 --- a/src/libexpr/get-drvs.cc +++ b/src/libexpr/get-drvs.cc @@ -1,8 +1,8 @@ -#include "get-drvs.hh" -#include "eval-inline.hh" -#include "derivations.hh" -#include "store-api.hh" -#include "path-with-outputs.hh" +#include "nix/expr/get-drvs.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/store/derivations.hh" +#include "nix/store/store-api.hh" +#include "nix/store/path-with-outputs.hh" #include #include @@ -117,7 +117,7 @@ PackageInfo::Outputs PackageInfo::queryOutputs(bool withPaths, bool onlyOutputsT state->forceList(*i->value, i->pos, "while evaluating the 'outputs' attribute of a derivation"); /* For each output... */ - for (auto elem : i->value->listItems()) { + for (auto elem : i->value->listView()) { std::string output(state->forceStringNoCtx(*elem, i->pos, "while evaluating the name of an output of a derivation")); if (withPaths) { @@ -159,7 +159,7 @@ PackageInfo::Outputs PackageInfo::queryOutputs(bool withPaths, bool onlyOutputsT /* ^ this shows during `nix-env -i` right under the bad derivation */ if (!outTI->isList()) throw errMsg; Outputs result; - for (auto elem : outTI->listItems()) { + for (auto elem : outTI->listView()) { if (elem->type() != nString) throw errMsg; auto out = outputs.find(elem->c_str()); if (out == outputs.end()) throw errMsg; @@ -206,7 +206,7 @@ bool PackageInfo::checkMeta(Value & v) { state->forceValue(v, v.determinePos(noPos)); if (v.type() == nList) { - for (auto elem : v.listItems()) + for (auto elem : v.listView()) if (!checkMeta(*elem)) return false; return true; } @@ -400,7 +400,8 @@ static void getDerivations(EvalState & state, Value & vIn, } else if (v.type() == nList) { - for (auto [n, elem] : enumerate(v.listItems())) { + auto listView = v.listView(); + for (auto [n, elem] : enumerate(listView)) { std::string pathPrefix2 = addToPath(pathPrefix, fmt("%d", n)); if (getDerivation(state, *elem, pathPrefix2, drvs, done, ignoreAssertionFailures)) getDerivations(state, *elem, pathPrefix2, autoArgs, drvs, done, ignoreAssertionFailures); diff --git a/src/libexpr/imported-drv-to-derivation.nix b/src/libexpr/imported-drv-to-derivation.nix index eab8b050e8f..e2cf7fd2652 100644 --- a/src/libexpr/imported-drv-to-derivation.nix +++ b/src/libexpr/imported-drv-to-derivation.nix @@ -1,21 +1,27 @@ -attrs @ { drvPath, outputs, name, ... }: +attrs@{ + drvPath, + outputs, + name, + ... +}: let - commonAttrs = (builtins.listToAttrs outputsList) // - { all = map (x: x.value) outputsList; - inherit drvPath name; - type = "derivation"; - }; + commonAttrs = (builtins.listToAttrs outputsList) // { + all = map (x: x.value) outputsList; + inherit drvPath name; + type = "derivation"; + }; - outputToAttrListElement = outputName: - { name = outputName; - value = commonAttrs // { - outPath = builtins.getAttr outputName attrs; - inherit outputName; - }; + outputToAttrListElement = outputName: { + name = outputName; + value = commonAttrs // { + outPath = builtins.getAttr outputName attrs; + inherit outputName; }; - + }; + outputsList = map outputToAttrListElement outputs; - -in (builtins.head outputsList).value + +in +(builtins.head outputsList).value diff --git a/src/libexpr/attr-path.hh b/src/libexpr/include/nix/expr/attr-path.hh similarity index 95% rename from src/libexpr/attr-path.hh rename to src/libexpr/include/nix/expr/attr-path.hh index eb00ffb93e4..66a3f4e00ef 100644 --- a/src/libexpr/attr-path.hh +++ b/src/libexpr/include/nix/expr/attr-path.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "eval.hh" +#include "nix/expr/eval.hh" #include #include diff --git a/src/libexpr/attr-set.hh b/src/libexpr/include/nix/expr/attr-set.hh similarity index 97% rename from src/libexpr/attr-set.hh rename to src/libexpr/include/nix/expr/attr-set.hh index 4df9a1acdc9..c44e8a6b9a2 100644 --- a/src/libexpr/attr-set.hh +++ b/src/libexpr/include/nix/expr/attr-set.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "nixexpr.hh" -#include "symbol-table.hh" +#include "nix/expr/nixexpr.hh" +#include "nix/expr/symbol-table.hh" #include @@ -23,7 +23,7 @@ struct Attr way we keep Attr size at two words with no wasted space. */ Symbol name; PosIdx pos; - Value * value; + Value * value = nullptr; Attr(Symbol name, Value * value, PosIdx pos = noPos) : name(name), pos(pos), value(value) { }; Attr() { }; diff --git a/src/libexpr/eval-cache.hh b/src/libexpr/include/nix/expr/eval-cache.hh similarity index 88% rename from src/libexpr/eval-cache.hh rename to src/libexpr/include/nix/expr/eval-cache.hh index b1911e3a4f7..31873f7a33c 100644 --- a/src/libexpr/eval-cache.hh +++ b/src/libexpr/include/nix/expr/eval-cache.hh @@ -1,9 +1,9 @@ #pragma once ///@file -#include "sync.hh" -#include "hash.hh" -#include "eval.hh" +#include "nix/util/sync.hh" +#include "nix/util/hash.hh" +#include "nix/expr/eval.hh" #include #include @@ -90,7 +90,7 @@ class AttrCursor : public std::enable_shared_from_this friend struct CachedEvalError; ref root; - typedef std::optional, Symbol>> Parent; + using Parent = std::optional, Symbol>>; Parent parent; RootValue _value; std::optional> cachedValue; @@ -99,6 +99,14 @@ class AttrCursor : public std::enable_shared_from_this Value & getValue(); + /** + * If `cachedValue` is unset, try to initialize it from the + * database. It is not an error if it does not exist. Throw a + * `CachedEvalError` exception if it does exist but has type + * `AttrType::Failed`. + */ + void fetchCachedValue(); + public: AttrCursor( diff --git a/src/libexpr/eval-error.hh b/src/libexpr/include/nix/expr/eval-error.hh similarity index 97% rename from src/libexpr/eval-error.hh rename to src/libexpr/include/nix/expr/eval-error.hh index ed004eb53a0..6f4c37f9066 100644 --- a/src/libexpr/eval-error.hh +++ b/src/libexpr/include/nix/expr/eval-error.hh @@ -1,7 +1,7 @@ #pragma once -#include "error.hh" -#include "pos-idx.hh" +#include "nix/util/error.hh" +#include "nix/util/pos-idx.hh" namespace nix { @@ -54,6 +54,7 @@ MakeError(TypeError, EvalError); MakeError(UndefinedVarError, EvalError); MakeError(MissingArgumentError, EvalError); MakeError(InfiniteRecursionError, EvalError); +MakeError(IFDError, EvalBaseError); struct InvalidPathError : public EvalError { diff --git a/src/libexpr/eval-gc.hh b/src/libexpr/include/nix/expr/eval-gc.hh similarity index 84% rename from src/libexpr/eval-gc.hh rename to src/libexpr/include/nix/expr/eval-gc.hh index f3b699b54a0..25144d40c1d 100644 --- a/src/libexpr/eval-gc.hh +++ b/src/libexpr/include/nix/expr/eval-gc.hh @@ -3,7 +3,10 @@ #include -#if HAVE_BOEHMGC +// For `NIX_USE_BOEHMGC`, and if that's set, `GC_THREADS` +#include "nix/expr/config.hh" + +#if NIX_USE_BOEHMGC # define GC_INCLUDE_NEW @@ -43,7 +46,7 @@ void initGC(); */ void assertGCInitialized(); -#ifdef HAVE_BOEHMGC +#if NIX_USE_BOEHMGC /** * The number of GC cycles since initGC(). */ diff --git a/src/libexpr/eval-inline.hh b/src/libexpr/include/nix/expr/eval-inline.hh similarity index 87% rename from src/libexpr/eval-inline.hh rename to src/libexpr/include/nix/expr/eval-inline.hh index 631c0f39610..7d13d7cc707 100644 --- a/src/libexpr/eval-inline.hh +++ b/src/libexpr/include/nix/expr/eval-inline.hh @@ -1,10 +1,13 @@ #pragma once ///@file -#include "print.hh" -#include "eval.hh" -#include "eval-error.hh" -#include "eval-settings.hh" +#include "nix/expr/print.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/eval-error.hh" +#include "nix/expr/eval-settings.hh" + +// For `NIX_USE_BOEHMGC`, and if that's set, `GC_THREADS` +#include "nix/expr/config.hh" namespace nix { @@ -15,7 +18,7 @@ namespace nix { inline void * allocBytes(size_t n) { void * p; -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC p = GC_MALLOC(n); #else p = calloc(n, 1); @@ -28,7 +31,7 @@ inline void * allocBytes(size_t n) [[gnu::always_inline]] Value * EvalState::allocValue() { -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC /* We use the boehm batch allocator to speed up allocations of Values (of which there are many). GC_malloc_many returns a linked list of objects of the given size, where the first word of each object is also the pointer to the next object in the list. This also means that we @@ -60,7 +63,7 @@ Env & EvalState::allocEnv(size_t size) Env * env; -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC if (size == 1) { /* see allocValue for explanations. */ if (!*env1AllocCache) { @@ -86,9 +89,9 @@ Env & EvalState::allocEnv(size_t size) void EvalState::forceValue(Value & v, const PosIdx pos) { if (v.isThunk()) { - Env * env = v.payload.thunk.env; + Env * env = v.thunk().env; assert(env || v.isBlackhole()); - Expr * expr = v.payload.thunk.expr; + Expr * expr = v.thunk().expr; try { v.mkBlackhole(); //checkInterrupt(); @@ -103,7 +106,7 @@ void EvalState::forceValue(Value & v, const PosIdx pos) } } else if (v.isApp()) - callFunction(*v.payload.app.left, *v.payload.app.right, v, pos); + callFunction(*v.app().left, *v.app().right, v, pos); } @@ -146,7 +149,7 @@ inline void EvalState::forceList(Value & v, const PosIdx pos, std::string_view e [[gnu::always_inline]] inline CallDepth EvalState::addCallDepth(const PosIdx pos) { if (callDepth > settings.maxCallDepth) - error("stack overflow; max-call-depth exceeded").atPos(pos).debugThrow(); + error("stack overflow; max-call-depth exceeded").atPos(pos).debugThrow(); return CallDepth(callDepth); }; diff --git a/src/libexpr/include/nix/expr/eval-profiler-settings.hh b/src/libexpr/include/nix/expr/eval-profiler-settings.hh new file mode 100644 index 00000000000..a94cde042ea --- /dev/null +++ b/src/libexpr/include/nix/expr/eval-profiler-settings.hh @@ -0,0 +1,16 @@ +#pragma once +///@file + +#include "nix/util/configuration.hh" + +namespace nix { + +enum struct EvalProfilerMode { disabled, flamegraph }; + +template<> +EvalProfilerMode BaseSetting::parse(const std::string & str) const; + +template<> +std::string BaseSetting::to_string() const; + +} diff --git a/src/libexpr/include/nix/expr/eval-profiler.hh b/src/libexpr/include/nix/expr/eval-profiler.hh new file mode 100644 index 00000000000..21629eebc14 --- /dev/null +++ b/src/libexpr/include/nix/expr/eval-profiler.hh @@ -0,0 +1,114 @@ +#pragma once +/** + * @file + * + * Evaluation profiler interface definitions and builtin implementations. + */ + +#include "nix/util/ref.hh" + +#include +#include +#include +#include +#include + +namespace nix { + +class EvalState; +class PosIdx; +struct Value; + +class EvalProfiler +{ +public: + enum Hook { + preFunctionCall, + postFunctionCall, + }; + + static constexpr std::size_t numHooks = Hook::postFunctionCall + 1; + using Hooks = std::bitset; + +private: + std::optional neededHooks; + +protected: + /** Invalidate the cached neededHooks. */ + void invalidateNeededHooks() + { + neededHooks = std::nullopt; + } + + /** + * Get which hooks need to be called. + * + * This is the actual implementation which has to be defined by subclasses. + * Public API goes through the needsHooks, which is a + * non-virtual interface (NVI) which caches the return value. + */ + virtual Hooks getNeededHooksImpl() const + { + return Hooks{}; + } + +public: + /** + * Hook called in the EvalState::callFunction preamble. + * Gets called only if (getNeededHooks().test(Hook::preFunctionCall)) is true. + * + * @param state Evaluator state. + * @param v Function being invoked. + * @param args Function arguments. + * @param pos Function position. + */ + virtual void preFunctionCallHook(EvalState & state, const Value & v, std::span args, const PosIdx pos); + + /** + * Hook called on EvalState::callFunction exit. + * Gets called only if (getNeededHooks().test(Hook::postFunctionCall)) is true. + * + * @param state Evaluator state. + * @param v Function being invoked. + * @param args Function arguments. + * @param pos Function position. + */ + virtual void postFunctionCallHook(EvalState & state, const Value & v, std::span args, const PosIdx pos); + + virtual ~EvalProfiler() = default; + + /** + * Get which hooks need to be invoked for this EvalProfiler instance. + */ + Hooks getNeededHooks() + { + if (neededHooks.has_value()) + return *neededHooks; + return *(neededHooks = getNeededHooksImpl()); + } +}; + +/** + * Profiler that invokes multiple profilers at once. + */ +class MultiEvalProfiler : public EvalProfiler +{ + std::vector> profilers; + + [[gnu::noinline]] Hooks getNeededHooksImpl() const override; + +public: + MultiEvalProfiler() = default; + + /** Register a profiler instance. */ + void addProfiler(ref profiler); + + [[gnu::noinline]] void + preFunctionCallHook(EvalState & state, const Value & v, std::span args, const PosIdx pos) override; + [[gnu::noinline]] void + postFunctionCallHook(EvalState & state, const Value & v, std::span args, const PosIdx pos) override; +}; + +ref makeSampleStackProfiler(EvalState & state, std::filesystem::path profileFile, uint64_t frequency); + +} diff --git a/src/libexpr/eval-settings.hh b/src/libexpr/include/nix/expr/eval-settings.hh similarity index 81% rename from src/libexpr/eval-settings.hh rename to src/libexpr/include/nix/expr/eval-settings.hh index a8fcce539d7..eee3b0f0e76 100644 --- a/src/libexpr/eval-settings.hh +++ b/src/libexpr/include/nix/expr/eval-settings.hh @@ -1,18 +1,19 @@ #pragma once ///@file -#include "config.hh" -#include "ref.hh" -#include "source-path.hh" +#include "nix/expr/eval-profiler-settings.hh" +#include "nix/util/configuration.hh" +#include "nix/util/source-path.hh" namespace nix { class EvalState; +struct PrimOp; struct EvalSettings : Config { /** - * Function used to interpet look path entries of a given scheme. + * Function used to interpret look path entries of a given scheme. * * The argument is the non-scheme part of the lookup path entry (see * `LookupPathHooks` below). @@ -51,6 +52,8 @@ struct EvalSettings : Config LookupPathHooks lookupPathHooks; + std::vector extraPrimOps; + Setting enableNativeCode{this, false, "allow-unsafe-native-code-during-evaluation", R"( Enable built-in functions that allow executing native code. @@ -129,7 +132,7 @@ struct EvalSettings : Config Setting restrictEval{ this, false, "restrict-eval", R"( - If set to `true`, the Nix evaluator will not allow access to any + If set to `true`, the Nix evaluator doesn't allow access to any files outside of [`builtins.nixPath`](@docroot@/language/builtins.md#builtins-nixPath), or to URIs outside of @@ -149,14 +152,24 @@ struct EvalSettings : Config )" }; + Setting traceImportFromDerivation{ + this, false, "trace-import-from-derivation", + R"( + By default, Nix allows [Import from Derivation](@docroot@/language/import-from-derivation.md). + + When this setting is `true`, Nix logs a warning indicating that it performed such an import. + This option has no effect if `allow-import-from-derivation` is disabled. + )" + }; + Setting enableImportFromDerivation{ this, true, "allow-import-from-derivation", R"( By default, Nix allows [Import from Derivation](@docroot@/language/import-from-derivation.md). - With this option set to `false`, Nix will throw an error when evaluating an expression that uses this feature, + With this option set to `false`, Nix throws an error when evaluating an expression that uses this feature, even when the required store object is readily available. - This ensures that evaluation will not require any builds to take place, + This ensures that evaluation doesn't require any builds to take place, regardless of the state of the store. )"}; @@ -175,8 +188,8 @@ struct EvalSettings : Config Setting traceFunctionCalls{this, false, "trace-function-calls", R"( - If set to `true`, the Nix evaluator will trace every function call. - Nix will print a log message at the "vomit" level for every function + If set to `true`, the Nix evaluator traces every function call. + Nix prints a log message at the "vomit" level for every function entrance and function exit. function-trace entered undefined position at 1565795816999559622 @@ -191,6 +204,29 @@ struct EvalSettings : Config `flamegraph.pl`. )"}; + Setting evalProfilerMode{this, EvalProfilerMode::disabled, "eval-profiler", + R"( + Enables evaluation profiling. The following modes are supported: + + * `flamegraph` stack sampling profiler. Outputs folded format, one line per stack (suitable for `flamegraph.pl` and compatible tools). + + Use [`eval-profile-file`](#conf-eval-profile-file) to specify where the profile is saved. + + See [Using the `eval-profiler`](@docroot@/advanced-topics/eval-profiler.md). + )"}; + + Setting evalProfileFile{this, "nix.profile", "eval-profile-file", + R"( + Specifies the file where [evaluation profile](#conf-eval-profiler) is saved. + )"}; + + Setting evalProfilerFrequency{this, 99, "eval-profiler-frequency", + R"( + Specifies the sampling rate in hertz for sampling evaluation profilers. + Use `0` to sample the stack after each function call. + See [`eval-profiler`](#conf-eval-profiler). + )"}; + Setting useEvalCache{this, true, "eval-cache", R"( Whether to use the flake evaluation cache. @@ -200,8 +236,8 @@ struct EvalSettings : Config Setting ignoreExceptionsDuringTry{this, false, "ignore-try", R"( - If set to true, ignore exceptions inside 'tryEval' calls when evaluating nix expressions in - debug mode (using the --debugger flag). By default the debugger will pause on all exceptions. + If set to true, ignore exceptions inside 'tryEval' calls when evaluating Nix expressions in + debug mode (using the --debugger flag). By default the debugger pauses on all exceptions. )"}; Setting traceVerbose{this, false, "trace-verbose", @@ -213,7 +249,7 @@ struct EvalSettings : Config Setting builtinsTraceDebugger{this, false, "debugger-on-trace", R"( If set to true and the `--debugger` flag is given, the following functions - will enter the debugger like [`builtins.break`](@docroot@/language/builtins.md#builtins-break). + enter the debugger like [`builtins.break`](@docroot@/language/builtins.md#builtins-break): * [`builtins.trace`](@docroot@/language/builtins.md#builtins-trace) * [`builtins.traceVerbose`](@docroot@/language/builtins.md#builtins-traceVerbose) @@ -235,7 +271,7 @@ struct EvalSettings : Config Setting builtinsAbortOnWarn{this, false, "abort-on-warn", R"( - If set to true, [`builtins.warn`](@docroot@/language/builtins.md#builtins-warn) will throw an error when logging a warning. + If set to true, [`builtins.warn`](@docroot@/language/builtins.md#builtins-warn) throws an error when logging a warning. This will give you a stack trace that leads to the location of the warning. diff --git a/src/libexpr/eval.hh b/src/libexpr/include/nix/expr/eval.hh similarity index 88% rename from src/libexpr/eval.hh rename to src/libexpr/include/nix/expr/eval.hh index 3ac3c8a8a6f..27294d11403 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/include/nix/expr/eval.hh @@ -1,20 +1,24 @@ #pragma once ///@file -#include "attr-set.hh" -#include "eval-error.hh" -#include "types.hh" -#include "value.hh" -#include "nixexpr.hh" -#include "symbol-table.hh" -#include "config.hh" -#include "experimental-features.hh" -#include "position.hh" -#include "pos-table.hh" -#include "source-accessor.hh" -#include "search-path.hh" -#include "repl-exit-status.hh" -#include "ref.hh" +#include "nix/expr/attr-set.hh" +#include "nix/expr/eval-error.hh" +#include "nix/expr/eval-profiler.hh" +#include "nix/util/types.hh" +#include "nix/expr/value.hh" +#include "nix/expr/nixexpr.hh" +#include "nix/expr/symbol-table.hh" +#include "nix/util/configuration.hh" +#include "nix/util/experimental-features.hh" +#include "nix/util/position.hh" +#include "nix/util/pos-table.hh" +#include "nix/util/source-accessor.hh" +#include "nix/expr/search-path.hh" +#include "nix/expr/repl-exit-status.hh" +#include "nix/util/ref.hh" + +// For `NIX_USE_BOEHMGC`, and if that's set, `GC_THREADS` +#include "nix/expr/config.hh" #include #include @@ -30,7 +34,10 @@ namespace nix { constexpr size_t maxPrimOpArity = 8; class Store; -namespace fetchers { struct Settings; } +namespace fetchers { +struct Settings; +struct InputCache; +} struct EvalSettings; class EvalState; class StorePath; @@ -159,7 +166,7 @@ void printEnvBindings(const SymbolTable & st, const StaticEnv & se, const Env & std::unique_ptr mapStaticEnvBindings(const SymbolTable & st, const StaticEnv & se, const Env & env); -void copyContext(const Value & v, NixStringContext & context); +void copyContext(const Value & v, NixStringContext & context, const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); std::string printValue(EvalState & state, Value & v); @@ -171,11 +178,28 @@ struct RegexCache; std::shared_ptr makeRegexCache(); struct DebugTrace { - std::shared_ptr pos; + /* WARNING: Converting PosIdx -> Pos should be done with extra care. This is + due to the fact that operator[] of PosTable is incredibly expensive. */ + std::variant pos; const Expr & expr; const Env & env; HintFmt hint; bool isError; + + Pos getPos(const PosTable & table) const + { + return std::visit( + overloaded{ + [&](PosIdx idx) { + // Prefer direct pos, but if noPos then try the expr. + if (!idx) + idx = expr.getPos(); + return table[idx]; + }, + [&](Pos pos) { return pos; }, + }, + pos); + } }; class EvalState : public std::enable_shared_from_this @@ -189,7 +213,7 @@ public: const Symbol sWith, sOutPath, sDrvPath, sType, sMeta, sName, sValue, sSystem, sOverrides, sOutputs, sOutputName, sIgnoreNulls, sFile, sLine, sColumn, sFunctor, sToString, - sRight, sWrong, sStructuredAttrs, + sRight, sWrong, sStructuredAttrs, sJson, sAllowedReferences, sAllowedRequisites, sDisallowedReferences, sDisallowedRequisites, sMaxSize, sMaxClosureSize, sBuilder, sArgs, @@ -245,6 +269,11 @@ public: /** `"unknown"` */ Value vStringUnknown; + /** + * The accessor corresponding to `store`. + */ + const ref storeFS; + /** * The accessor for the root filesystem. */ @@ -257,14 +286,12 @@ public: /** * In-memory filesystem for internal, non-user-callable Nix - * expressions like call-flake.nix. + * expressions like `derivation.nix`. */ const ref internalFS; const SourcePath derivationInternal; - const SourcePath callFlakeInternal; - /** * Store used to materialise .drv files. */ @@ -277,6 +304,8 @@ public: RootValue vImportedDrvToDerivation = nullptr; + ref inputCache; + /** * Debugger */ @@ -354,7 +383,7 @@ private: */ std::shared_ptr regexCache; -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC /** * Allocation cache for GC'd Value objects. */ @@ -389,6 +418,15 @@ public: */ SourcePath rootPath(PathView path); + /** + * Return a `SourcePath` that refers to `path` in the store. + * + * For now, this has to also be within the root filesystem for + * backwards compat, but for Windows and maybe also pure eval, we'll + * probably want to do something different. + */ + SourcePath storePath(const StorePath & path); + /** * Allow access to a path. */ @@ -400,6 +438,11 @@ public: */ void allowPath(const StorePath & storePath); + /** + * Allow access to the closure of a store path. + */ + void allowClosure(const StorePath & storePath); + /** * Allow access to a store path and return it as a string. */ @@ -407,17 +450,6 @@ public: void checkURI(const std::string & uri); - /** - * When using a diverted store and 'path' is in the Nix store, map - * 'path' to the diverted location (e.g. /nix/store/foo is mapped - * to /home/alice/my-nix/nix/store/foo). However, this is only - * done if the context is not empty, since otherwise we're - * probably trying to read from the actual /nix/store. This is - * intended to distinguish between import-from-derivation and - * sources stored in the actual /nix/store. - */ - Path toRealPath(const Path & path, const NixStringContext & context); - /** * Parse a Nix expression from the specified file. */ @@ -507,9 +539,14 @@ public: */ void forceFunction(Value & v, const PosIdx pos, std::string_view errorCtx); std::string_view forceString(Value & v, const PosIdx pos, std::string_view errorCtx); - std::string_view forceString(Value & v, NixStringContext & context, const PosIdx pos, std::string_view errorCtx); + std::string_view forceString(Value & v, NixStringContext & context, const PosIdx pos, std::string_view errorCtx, const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); std::string_view forceStringNoCtx(Value & v, const PosIdx pos, std::string_view errorCtx); + /** + * Get attribute from an attribute set and throw an error if it doesn't exist. + */ + Bindings::const_iterator getAttr(Symbol attrSym, const Bindings * attrSet, std::string_view errorCtx); + template [[gnu::noinline]] void addErrorTrace(Error & e, const Args & ... formatArgs) const; @@ -559,7 +596,7 @@ public: /** * Part of `coerceToSingleDerivedPath()` without any store IO which is exposed for unit testing only. */ - std::pair coerceToSingleDerivedPathUnchecked(const PosIdx pos, Value & v, std::string_view errorCtx); + std::pair coerceToSingleDerivedPathUnchecked(const PosIdx pos, Value & v, std::string_view errorCtx, const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); /** * Coerce to `SingleDerivedPath`. @@ -578,7 +615,7 @@ public: */ SingleDerivedPath coerceToSingleDerivedPath(const PosIdx pos, Value & v, std::string_view errorCtx); -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC /** A GC root for the baseEnv reference. */ std::shared_ptr baseEnvP; #endif @@ -613,7 +650,7 @@ private: unsigned int baseEnvDispl = 0; - void createBaseEnv(); + void createBaseEnv(const EvalSettings & settings); Value * addConstant(const std::string & name, Value & v, Constant info); @@ -699,7 +736,7 @@ public: */ void assertEqValues(Value & v1, Value & v2, const PosIdx pos, std::string_view errorCtx); - bool isFunctor(Value & fun); + bool isFunctor(const Value & fun) const; void callFunction(Value & fun, std::span args, Value & vRes, const PosIdx pos); @@ -815,6 +852,15 @@ public: */ [[nodiscard]] StringMap realiseContext(const NixStringContext & context, StorePathSet * maybePaths = nullptr, bool isIFD = true); + /** + * Realise the given string with context, and return the string with outputs instead of downstream output placeholders. + * @param[in] str the string to realise + * @param[out] paths all referenced store paths will be added to this set + * @return the realised string + * @throw EvalError if the value is not a string, path or derivation (see `coerceToString`) + */ + std::string realiseString(Value & str, StorePathSet * storePathsOutMaybe, bool isIFD = true, const PosIdx pos = noPos); + /* Call the binary path filter predicate used builtins.path etc. */ bool callPathFilter( Value * filterFun, @@ -863,6 +909,9 @@ private: typedef std::map FunctionCalls; FunctionCalls functionCalls; + /** Evaluation/call profiler. */ + MultiEvalProfiler profiler; + void incrFunctionCall(ExprLambda * fun); typedef std::map AttrSelects; @@ -917,4 +966,4 @@ bool isAllowedURI(std::string_view uri, const Strings & allowedPaths); } -#include "eval-inline.hh" +#include "nix/expr/eval-inline.hh" diff --git a/src/libexpr/include/nix/expr/function-trace.hh b/src/libexpr/include/nix/expr/function-trace.hh new file mode 100644 index 00000000000..ed1fc645203 --- /dev/null +++ b/src/libexpr/include/nix/expr/function-trace.hh @@ -0,0 +1,25 @@ +#pragma once +///@file + +#include "nix/expr/eval.hh" +#include "nix/expr/eval-profiler.hh" + +namespace nix { + +class FunctionCallTrace : public EvalProfiler +{ + Hooks getNeededHooksImpl() const override + { + return Hooks().set(preFunctionCall).set(postFunctionCall); + } + +public: + FunctionCallTrace() = default; + + [[gnu::noinline]] void + preFunctionCallHook(EvalState & state, const Value & v, std::span args, const PosIdx pos) override; + [[gnu::noinline]] void + postFunctionCallHook(EvalState & state, const Value & v, std::span args, const PosIdx pos) override; +}; + +} diff --git a/src/libexpr/gc-small-vector.hh b/src/libexpr/include/nix/expr/gc-small-vector.hh similarity index 95% rename from src/libexpr/gc-small-vector.hh rename to src/libexpr/include/nix/expr/gc-small-vector.hh index 8330dd2dca1..ad4503de72a 100644 --- a/src/libexpr/gc-small-vector.hh +++ b/src/libexpr/include/nix/expr/gc-small-vector.hh @@ -2,7 +2,7 @@ #include -#include "value.hh" +#include "nix/expr/value.hh" namespace nix { diff --git a/src/libexpr/get-drvs.hh b/src/libexpr/include/nix/expr/get-drvs.hh similarity index 97% rename from src/libexpr/get-drvs.hh rename to src/libexpr/include/nix/expr/get-drvs.hh index e4e277af8cc..0787c44a8b4 100644 --- a/src/libexpr/get-drvs.hh +++ b/src/libexpr/include/nix/expr/get-drvs.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "eval.hh" -#include "path.hh" +#include "nix/expr/eval.hh" +#include "nix/store/path.hh" #include #include diff --git a/src/libexpr/json-to-value.hh b/src/libexpr/include/nix/expr/json-to-value.hh similarity index 87% rename from src/libexpr/json-to-value.hh rename to src/libexpr/include/nix/expr/json-to-value.hh index 3c8fa5cc00a..b01d63bfe63 100644 --- a/src/libexpr/json-to-value.hh +++ b/src/libexpr/include/nix/expr/json-to-value.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "error.hh" +#include "nix/util/error.hh" #include diff --git a/src/libexpr/include/nix/expr/meson.build b/src/libexpr/include/nix/expr/meson.build new file mode 100644 index 00000000000..333490ee499 --- /dev/null +++ b/src/libexpr/include/nix/expr/meson.build @@ -0,0 +1,38 @@ +# Public headers directory + +include_dirs = [include_directories('../..')] + +config_pub_h = configure_file( + configuration : configdata_pub, + output : 'config.hh', +) + +headers = [config_pub_h] + files( + 'attr-path.hh', + 'attr-set.hh', + 'eval-cache.hh', + 'eval-error.hh', + 'eval-gc.hh', + 'eval-inline.hh', + 'eval-profiler-settings.hh', + 'eval-profiler.hh', + 'eval-settings.hh', + 'eval.hh', + 'function-trace.hh', + 'gc-small-vector.hh', + 'get-drvs.hh', + 'json-to-value.hh', + 'nixexpr.hh', + 'parser-state.hh', + 'primops.hh', + 'print-ambiguous.hh', + 'print-options.hh', + 'print.hh', + 'repl-exit-status.hh', + 'search-path.hh', + 'symbol-table.hh', + 'value-to-json.hh', + 'value-to-xml.hh', + 'value.hh', + 'value/context.hh', +) diff --git a/src/libexpr/nixexpr.hh b/src/libexpr/include/nix/expr/nixexpr.hh similarity index 96% rename from src/libexpr/nixexpr.hh rename to src/libexpr/include/nix/expr/nixexpr.hh index 2950ff1fd7c..6ede91948e0 100644 --- a/src/libexpr/nixexpr.hh +++ b/src/libexpr/include/nix/expr/nixexpr.hh @@ -4,10 +4,10 @@ #include #include -#include "value.hh" -#include "symbol-table.hh" -#include "eval-error.hh" -#include "pos-idx.hh" +#include "nix/expr/value.hh" +#include "nix/expr/symbol-table.hh" +#include "nix/expr/eval-error.hh" +#include "nix/util/pos-idx.hh" namespace nix { @@ -65,7 +65,7 @@ struct DocComment { struct AttrName { Symbol symbol; - Expr * expr; + Expr * expr = nullptr; AttrName(Symbol s) : symbol(s) {}; AttrName(Expr * e) : expr(e) {}; }; @@ -159,7 +159,7 @@ struct ExprVar : Expr `nullptr`: Not from a `with`. Valid pointer: the nearest, innermost `with` expression to query first. */ - ExprWith * fromWith; + ExprWith * fromWith = nullptr; /* In the former case, the value is obtained by going `level` levels up from the current environment and getting the @@ -167,8 +167,8 @@ struct ExprVar : Expr value is obtained by getting the attribute named `name` from the set stored in the environment that is `level` levels up from the current one.*/ - Level level; - Displacement displ; + Level level = 0; + Displacement displ = 0; ExprVar(Symbol name) : name(name) { }; ExprVar(const PosIdx & pos, Symbol name) : pos(pos), name(name) { }; @@ -242,7 +242,7 @@ struct ExprAttrs : Expr Kind kind; Expr * e; PosIdx pos; - Displacement displ; // displacement + Displacement displ = 0; // displacement AttrDef(Expr * e, const PosIdx & pos, Kind kind = Kind::Plain) : kind(kind), e(e), pos(pos) { }; AttrDef() { }; @@ -306,6 +306,9 @@ struct Formal struct Formals { typedef std::vector Formals_; + /** + * @pre Sorted according to predicate (std::tie(a.name, a.pos) < std::tie(b.name, b.pos)). + */ Formals_ formals; bool ellipsis; @@ -480,13 +483,16 @@ extern ExprBlackHole eBlackHole; struct StaticEnv { ExprWith * isWith; - const StaticEnv * up; + std::shared_ptr up; // Note: these must be in sorted order. typedef std::vector> Vars; Vars vars; - StaticEnv(ExprWith * isWith, const StaticEnv * up, size_t expectedSize = 0) : isWith(isWith), up(up) { + StaticEnv(ExprWith * isWith, std::shared_ptr up, size_t expectedSize = 0) + : isWith(isWith) + , up(std::move(up)) + { vars.reserve(expectedSize); }; diff --git a/src/libexpr/parser-state.hh b/src/libexpr/include/nix/expr/parser-state.hh similarity index 99% rename from src/libexpr/parser-state.hh rename to src/libexpr/include/nix/expr/parser-state.hh index 21a880e8eb7..0505913d087 100644 --- a/src/libexpr/parser-state.hh +++ b/src/libexpr/include/nix/expr/parser-state.hh @@ -3,7 +3,7 @@ #include -#include "eval.hh" +#include "nix/expr/eval.hh" namespace nix { diff --git a/src/libexpr/primops.hh b/src/libexpr/include/nix/expr/primops.hh similarity index 92% rename from src/libexpr/primops.hh rename to src/libexpr/include/nix/expr/primops.hh index 9f76975db8d..0b4ecdd50dd 100644 --- a/src/libexpr/primops.hh +++ b/src/libexpr/include/nix/expr/primops.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "eval.hh" +#include "nix/expr/eval.hh" #include #include @@ -27,7 +27,12 @@ constexpr size_t conservativeStackReservation = 16; struct RegisterPrimOp { typedef std::vector PrimOps; - static PrimOps * primOps; + + static PrimOps & primOps() + { + static PrimOps primOps; + return primOps; + } /** * You can register a constant by passing an arity of 0. fun diff --git a/src/libexpr/print-ambiguous.hh b/src/libexpr/include/nix/expr/print-ambiguous.hh similarity index 89% rename from src/libexpr/print-ambiguous.hh rename to src/libexpr/include/nix/expr/print-ambiguous.hh index 50c260a9b84..9e5a27e6d6e 100644 --- a/src/libexpr/print-ambiguous.hh +++ b/src/libexpr/include/nix/expr/print-ambiguous.hh @@ -1,6 +1,7 @@ #pragma once -#include "value.hh" +#include "nix/expr/value.hh" +#include "nix/expr/symbol-table.hh" namespace nix { diff --git a/src/libexpr/print-options.hh b/src/libexpr/include/nix/expr/print-options.hh similarity index 99% rename from src/libexpr/print-options.hh rename to src/libexpr/include/nix/expr/print-options.hh index 080ba26b86c..9ad54e5323c 100644 --- a/src/libexpr/print-options.hh +++ b/src/libexpr/include/nix/expr/print-options.hh @@ -5,6 +5,7 @@ */ #include +#include namespace nix { diff --git a/src/libexpr/print.hh b/src/libexpr/include/nix/expr/print.hh similarity index 97% rename from src/libexpr/print.hh rename to src/libexpr/include/nix/expr/print.hh index 7ddda81b88f..ac9bf23a431 100644 --- a/src/libexpr/print.hh +++ b/src/libexpr/include/nix/expr/print.hh @@ -9,8 +9,8 @@ #include -#include "fmt.hh" -#include "print-options.hh" +#include "nix/util/fmt.hh" +#include "nix/expr/print-options.hh" namespace nix { diff --git a/src/libexpr/repl-exit-status.hh b/src/libexpr/include/nix/expr/repl-exit-status.hh similarity index 100% rename from src/libexpr/repl-exit-status.hh rename to src/libexpr/include/nix/expr/repl-exit-status.hh diff --git a/src/libexpr/search-path.hh b/src/libexpr/include/nix/expr/search-path.hh similarity index 97% rename from src/libexpr/search-path.hh rename to src/libexpr/include/nix/expr/search-path.hh index acd84363853..202527fd2fa 100644 --- a/src/libexpr/search-path.hh +++ b/src/libexpr/include/nix/expr/search-path.hh @@ -3,8 +3,8 @@ #include -#include "types.hh" -#include "comparator.hh" +#include "nix/util/types.hh" +#include "nix/util/comparator.hh" namespace nix { diff --git a/src/libexpr/include/nix/expr/symbol-table.hh b/src/libexpr/include/nix/expr/symbol-table.hh new file mode 100644 index 00000000000..4dedf3d913a --- /dev/null +++ b/src/libexpr/include/nix/expr/symbol-table.hh @@ -0,0 +1,267 @@ +#pragma once +///@file + +#include +#include "nix/expr/value.hh" +#include "nix/util/chunked-vector.hh" +#include "nix/util/error.hh" + +#include +#include + +namespace nix { + +class SymbolValue : protected Value +{ + friend class SymbolStr; + friend class SymbolTable; + + uint32_t size_; + uint32_t idx; + + SymbolValue() = default; + +public: + operator std::string_view() const noexcept + { + return {c_str(), size_}; + } +}; + +/** + * Symbols have the property that they can be compared efficiently + * (using an equality test), because the symbol table stores only one + * copy of each string. + */ +class Symbol +{ + friend class SymbolStr; + friend class SymbolTable; + +private: + uint32_t id; + + explicit Symbol(uint32_t id) noexcept : id(id) {} + +public: + Symbol() noexcept : id(0) {} + + [[gnu::always_inline]] + explicit operator bool() const noexcept { return id > 0; } + + auto operator<=>(const Symbol other) const noexcept { return id <=> other.id; } + bool operator==(const Symbol other) const noexcept { return id == other.id; } + + friend class std::hash; +}; + +/** + * This class mainly exists to give us an operator<< for ostreams. We could also + * return plain strings from SymbolTable, but then we'd have to wrap every + * instance of a symbol that is fmt()ed, which is inconvenient and error-prone. + */ +class SymbolStr +{ + friend class SymbolTable; + + constexpr static size_t chunkSize{8192}; + using SymbolValueStore = ChunkedVector; + + const SymbolValue * s; + + struct Key + { + using HashType = boost::hash; + + SymbolValueStore & store; + std::string_view s; + std::size_t hash; + std::pmr::polymorphic_allocator & alloc; + + Key(SymbolValueStore & store, std::string_view s, std::pmr::polymorphic_allocator & stringAlloc) + : store(store) + , s(s) + , hash(HashType{}(s)) + , alloc(stringAlloc) {} + }; + +public: + SymbolStr(const SymbolValue & s) noexcept : s(&s) {} + + SymbolStr(const Key & key) + { + auto size = key.s.size(); + if (size >= std::numeric_limits::max()) { + throw Error("Size of symbol exceeds 4GiB and cannot be stored"); + } + // for multi-threaded implementations: lock store and allocator here + const auto & [v, idx] = key.store.add(SymbolValue{}); + if (size == 0) { + v.mkString("", nullptr); + } else { + auto s = key.alloc.allocate(size + 1); + memcpy(s, key.s.data(), size); + s[size] = '\0'; + v.mkString(s, nullptr); + } + v.size_ = size; + v.idx = idx; + this->s = &v; + } + + bool operator == (std::string_view s2) const noexcept + { + return *s == s2; + } + + [[gnu::always_inline]] + const char * c_str() const noexcept + { + return s->c_str(); + } + + [[gnu::always_inline]] + operator std::string_view () const noexcept + { + return *s; + } + + friend std::ostream & operator <<(std::ostream & os, const SymbolStr & symbol); + + [[gnu::always_inline]] + bool empty() const noexcept + { + return s->size_ == 0; + } + + [[gnu::always_inline]] + size_t size() const noexcept + { + return s->size_; + } + + [[gnu::always_inline]] + const Value * valuePtr() const noexcept + { + return s; + } + + explicit operator Symbol() const noexcept + { + return Symbol{s->idx + 1}; + } + + struct Hash + { + using is_transparent = void; + using is_avalanching = std::true_type; + + std::size_t operator()(SymbolStr str) const + { + return Key::HashType{}(*str.s); + } + + std::size_t operator()(const Key & key) const noexcept + { + return key.hash; + } + }; + + struct Equal + { + using is_transparent = void; + + bool operator()(SymbolStr a, SymbolStr b) const noexcept + { + // strings are unique, so that a pointer comparison is OK + return a.s == b.s; + } + + bool operator()(SymbolStr a, const Key & b) const noexcept + { + return a == b.s; + } + + [[gnu::always_inline]] + bool operator()(const Key & a, SymbolStr b) const noexcept + { + return operator()(b, a); + } + }; +}; + +/** + * Symbol table used by the parser and evaluator to represent and look + * up identifiers and attributes efficiently. + */ +class SymbolTable +{ +private: + /** + * SymbolTable is an append only data structure. + * During its lifetime the monotonic buffer holds all strings and nodes, if the symbol set is node based. + */ + std::pmr::monotonic_buffer_resource buffer; + std::pmr::polymorphic_allocator stringAlloc{&buffer}; + SymbolStr::SymbolValueStore store{16}; + + /** + * Transparent lookup of string view for a pointer to a ChunkedVector entry -> return offset into the store. + * ChunkedVector references are never invalidated. + */ + boost::unordered_flat_set symbols{SymbolStr::chunkSize}; + +public: + + /** + * Converts a string into a symbol. + */ + Symbol create(std::string_view s) { + // Most symbols are looked up more than once, so we trade off insertion performance + // for lookup performance. + // FIXME: make this thread-safe. + return Symbol(*symbols.insert(SymbolStr::Key{store, s, stringAlloc}).first); + } + + std::vector resolve(const std::vector & symbols) const + { + std::vector result; + result.reserve(symbols.size()); + for (auto sym : symbols) + result.push_back((*this)[sym]); + return result; + } + + SymbolStr operator[](Symbol s) const + { + uint32_t idx = s.id - uint32_t(1); + if (idx >= store.size()) + unreachable(); + return store[idx]; + } + + [[gnu::always_inline]] + size_t size() const noexcept + { + return store.size(); + } + + size_t totalSize() const; + + template + void dump(T callback) const + { + store.forEach(callback); + } +}; + +} + +template<> +struct std::hash +{ + std::size_t operator()(const nix::Symbol & s) const noexcept + { + return std::hash{}(s.id); + } +}; diff --git a/src/libexpr/value-to-json.hh b/src/libexpr/include/nix/expr/value-to-json.hh similarity index 80% rename from src/libexpr/value-to-json.hh rename to src/libexpr/include/nix/expr/value-to-json.hh index 47ac90313b3..1a691134705 100644 --- a/src/libexpr/value-to-json.hh +++ b/src/libexpr/include/nix/expr/value-to-json.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "nixexpr.hh" -#include "eval.hh" +#include "nix/expr/nixexpr.hh" +#include "nix/expr/eval.hh" #include #include @@ -16,4 +16,7 @@ nlohmann::json printValueAsJSON(EvalState & state, bool strict, void printValueAsJSON(EvalState & state, bool strict, Value & v, const PosIdx pos, std::ostream & str, NixStringContext & context, bool copyToStore = true); + +MakeError(JSONSerializationError, Error); + } diff --git a/src/libexpr/value-to-xml.hh b/src/libexpr/include/nix/expr/value-to-xml.hh similarity index 79% rename from src/libexpr/value-to-xml.hh rename to src/libexpr/include/nix/expr/value-to-xml.hh index 6d702c0f236..e22325de5e4 100644 --- a/src/libexpr/value-to-xml.hh +++ b/src/libexpr/include/nix/expr/value-to-xml.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "nixexpr.hh" -#include "eval.hh" +#include "nix/expr/nixexpr.hh" +#include "nix/expr/eval.hh" #include #include diff --git a/src/libexpr/include/nix/expr/value.hh b/src/libexpr/include/nix/expr/value.hh new file mode 100644 index 00000000000..098effa29d1 --- /dev/null +++ b/src/libexpr/include/nix/expr/value.hh @@ -0,0 +1,1174 @@ +#pragma once +///@file + +#include +#include +#include +#include + +#include "nix/expr/eval-gc.hh" +#include "nix/expr/value/context.hh" +#include "nix/util/source-path.hh" +#include "nix/expr/print-options.hh" +#include "nix/util/checked-arithmetic.hh" + +#include + +namespace nix { + +struct Value; +class BindingsBuilder; + +/** + * Internal type discriminator, which is more detailed than `ValueType`, as + * it specifies the exact representation used (for types that have multiple + * possible representations). + * + * @warning The ordering is very significant. See ValueStorage::getInternalType() for details + * about how this is mapped into the alignment bits to save significant memory. + * This also restricts the number of internal types represented with distinct memory layouts. + */ +typedef enum { + tUninitialized = 0, + /* layout: Single/zero field payload */ + tInt = 1, + tBool, + tNull, + tFloat, + tExternal, + tPrimOp, + tAttrs, + /* layout: Pair of pointers payload */ + tListSmall, + tPrimOpApp, + tApp, + tThunk, + tLambda, + /* layout: Single untaggable field */ + tListN, + tString, + tPath, +} InternalType; + +/** + * This type abstracts over all actual value types in the language, + * grouping together implementation details like tList*, different function + * types, and types in non-normal form (so thunks and co.) + */ +typedef enum { + nThunk, + nInt, + nFloat, + nBool, + nString, + nPath, + nNull, + nAttrs, + nList, + nFunction, + nExternal, +} ValueType; + +class Bindings; +struct Env; +struct Expr; +struct ExprLambda; +struct ExprBlackHole; +struct PrimOp; +class Symbol; +class SymbolStr; +class PosIdx; +struct Pos; +class StorePath; +class EvalState; +class XMLWriter; +class Printer; + +using NixInt = checked::Checked; +using NixFloat = double; + +/** + * External values must descend from ExternalValueBase, so that + * type-agnostic nix functions (e.g. showType) can be implemented + */ +class ExternalValueBase +{ + friend std::ostream & operator<<(std::ostream & str, const ExternalValueBase & v); + friend class Printer; +protected: + /** + * Print out the value + */ + virtual std::ostream & print(std::ostream & str) const = 0; + +public: + /** + * Return a simple string describing the type + */ + virtual std::string showType() const = 0; + + /** + * Return a string to be used in builtins.typeOf + */ + virtual std::string typeOf() const = 0; + + /** + * Coerce the value to a string. Defaults to uncoercable, i.e. throws an + * error. + */ + virtual std::string coerceToString( + EvalState & state, const PosIdx & pos, NixStringContext & context, bool copyMore, bool copyToStore) const; + + /** + * Compare to another value of the same type. Defaults to uncomparable, + * i.e. always false. + */ + virtual bool operator==(const ExternalValueBase & b) const noexcept; + + /** + * Print the value as JSON. Defaults to unconvertable, i.e. throws an error + */ + virtual nlohmann::json + printValueAsJSON(EvalState & state, bool strict, NixStringContext & context, bool copyToStore = true) const; + + /** + * Print the value as XML. Defaults to unevaluated + */ + virtual void printValueAsXML( + EvalState & state, + bool strict, + bool location, + XMLWriter & doc, + NixStringContext & context, + PathSet & drvsSeen, + const PosIdx pos) const; + + virtual ~ExternalValueBase() {}; +}; + +std::ostream & operator<<(std::ostream & str, const ExternalValueBase & v); + +class ListBuilder +{ + const size_t size; + Value * inlineElems[2] = {nullptr, nullptr}; +public: + Value ** elems; + ListBuilder(EvalState & state, size_t size); + + // NOTE: Can be noexcept because we are just copying integral values and + // raw pointers. + ListBuilder(ListBuilder && x) noexcept + : size(x.size) + , inlineElems{x.inlineElems[0], x.inlineElems[1]} + , elems(size <= 2 ? inlineElems : x.elems) + { + } + + Value *& operator[](size_t n) + { + return elems[n]; + } + + typedef Value ** iterator; + + iterator begin() + { + return &elems[0]; + } + iterator end() + { + return &elems[size]; + } + + friend struct Value; +}; + +namespace detail { + +/** + * Implementation mixin class for defining the public types + * In can be inherited from by the actual ValueStorage implementations + * for free due to Empty Base Class Optimization (EBCO). + */ +struct ValueBase +{ + /** + * Strings in the evaluator carry a so-called `context` which + * is a list of strings representing store paths. This is to + * allow users to write things like + * + * "--with-freetype2-library=" + freetype + "/lib" + * + * where `freetype` is a derivation (or a source to be copied + * to the store). If we just concatenated the strings without + * keeping track of the referenced store paths, then if the + * string is used as a derivation attribute, the derivation + * will not have the correct dependencies in its inputDrvs and + * inputSrcs. + + * The semantics of the context is as follows: when a string + * with context C is used as a derivation attribute, then the + * derivations in C will be added to the inputDrvs of the + * derivation, and the other store paths in C will be added to + * the inputSrcs of the derivations. + + * For canonicity, the store paths should be in sorted order. + */ + struct StringWithContext + { + const char * c_str; + const char ** context; // must be in sorted order + }; + + struct Path + { + SourceAccessor * accessor; + const char * path; + }; + + struct Null + {}; + + struct ClosureThunk + { + Env * env; + Expr * expr; + }; + + struct FunctionApplicationThunk + { + Value *left, *right; + }; + + /** + * Like FunctionApplicationThunk, but must be a distinct type in order to + * resolve overloads to `tPrimOpApp` instead of `tApp`. + * This type helps with the efficient implementation of arity>=2 primop calls. + */ + struct PrimOpApplicationThunk + { + Value *left, *right; + }; + + struct Lambda + { + Env * env; + ExprLambda * fun; + }; + + using SmallList = std::array; + + struct List + { + size_t size; + Value * const * elems; + }; +}; + +template +struct PayloadTypeToInternalType +{}; + +/** + * All stored types must be distinct (not type aliases) for the purposes of + * overload resolution in setStorage. This ensures there's a bijection from + * InternalType <-> C++ type. + */ +#define NIX_VALUE_STORAGE_FOR_EACH_FIELD(MACRO) \ + MACRO(NixInt, integer, tInt) \ + MACRO(bool, boolean, tBool) \ + MACRO(ValueBase::StringWithContext, string, tString) \ + MACRO(ValueBase::Path, path, tPath) \ + MACRO(ValueBase::Null, null_, tNull) \ + MACRO(Bindings *, attrs, tAttrs) \ + MACRO(ValueBase::List, bigList, tListN) \ + MACRO(ValueBase::SmallList, smallList, tListSmall) \ + MACRO(ValueBase::ClosureThunk, thunk, tThunk) \ + MACRO(ValueBase::FunctionApplicationThunk, app, tApp) \ + MACRO(ValueBase::Lambda, lambda, tLambda) \ + MACRO(PrimOp *, primOp, tPrimOp) \ + MACRO(ValueBase::PrimOpApplicationThunk, primOpApp, tPrimOpApp) \ + MACRO(ExternalValueBase *, external, tExternal) \ + MACRO(NixFloat, fpoint, tFloat) + +#define NIX_VALUE_PAYLOAD_TYPE(T, FIELD_NAME, DISCRIMINATOR) \ + template<> \ + struct PayloadTypeToInternalType \ + { \ + static constexpr InternalType value = DISCRIMINATOR; \ + }; + +NIX_VALUE_STORAGE_FOR_EACH_FIELD(NIX_VALUE_PAYLOAD_TYPE) + +#undef NIX_VALUE_PAYLOAD_TYPE + +template +inline constexpr InternalType payloadTypeToInternalType = PayloadTypeToInternalType::value; + +} + +/** + * Discriminated union of types stored in the value. + * The union discriminator is @ref InternalType enumeration. + * + * This class can be specialized with a non-type template parameter + * of pointer size for more optimized data layouts on when pointer alignment + * bits can be used for storing the discriminator. + * + * All specializations of this type need to implement getStorage, setStorage and + * getInternalType methods. + */ +template +class ValueStorage : public detail::ValueBase +{ +protected: + using Payload = union + { +#define NIX_VALUE_STORAGE_DEFINE_FIELD(T, FIELD_NAME, DISCRIMINATOR) T FIELD_NAME; + NIX_VALUE_STORAGE_FOR_EACH_FIELD(NIX_VALUE_STORAGE_DEFINE_FIELD) +#undef NIX_VALUE_STORAGE_DEFINE_FIELD + }; + +private: + InternalType internalType = tUninitialized; + Payload payload; + +protected: +#define NIX_VALUE_STORAGE_GET_IMPL(K, FIELD_NAME, DISCRIMINATOR) \ + void getStorage(K & val) const noexcept \ + { \ + assert(internalType == DISCRIMINATOR); \ + val = payload.FIELD_NAME; \ + } + +#define NIX_VALUE_STORAGE_SET_IMPL(K, FIELD_NAME, DISCRIMINATOR) \ + void setStorage(K val) noexcept \ + { \ + payload.FIELD_NAME = val; \ + internalType = DISCRIMINATOR; \ + } + + NIX_VALUE_STORAGE_FOR_EACH_FIELD(NIX_VALUE_STORAGE_GET_IMPL) + NIX_VALUE_STORAGE_FOR_EACH_FIELD(NIX_VALUE_STORAGE_SET_IMPL) + +#undef NIX_VALUE_STORAGE_SET_IMPL +#undef NIX_VALUE_STORAGE_GET_IMPL +#undef NIX_VALUE_STORAGE_FOR_EACH_FIELD + + /** Get internal type currently occupying the storage. */ + InternalType getInternalType() const noexcept + { + return internalType; + } +}; + +namespace detail { + +/* Whether to use a specialization of ValueStorage that does bitpacking into + alignment niches. */ +template +inline constexpr bool useBitPackedValueStorage = (ptrSize == 8) && (__STDCPP_DEFAULT_NEW_ALIGNMENT__ >= 8); + +} // namespace detail + +/** + * Value storage that is optimized for 64 bit systems. + * Packs discriminator bits into the pointer alignment niches. + */ +template +class ValueStorage>> : public detail::ValueBase +{ + /* Needs a dependent type name in order for member functions (and + * potentially ill-formed bit casts) to be SFINAE'd out. + * + * Otherwise some member functions could possibly be instantiated for 32 bit + * systems and fail due to an unsatisfied constraint. + */ + template + struct PackedPointerTypeStruct + { + using type = std::uint64_t; + }; + + using PackedPointer = typename PackedPointerTypeStruct::type; + using Payload = std::array; + Payload payload = {}; + + static constexpr int discriminatorBits = 3; + static constexpr PackedPointer discriminatorMask = (PackedPointer(1) << discriminatorBits) - 1; + + /** + * The value is stored as a pair of 8-byte double words. All pointers are assumed + * to be 8-byte aligned. This gives us at most 6 bits of discriminator bits + * of free storage. In some cases when one double word can't be tagged the whole + * discriminator is stored in the first double word. + * + * The layout of discriminator bits is determined by the 3 bits of PrimaryDiscriminator, + * which are always stored in the lower 3 bits of the first dword of the payload. + * The memory layout has 3 types depending on the PrimaryDiscriminator value. + * + * PrimaryDiscriminator::pdSingleDWord - Only the second dword carries the data. + * That leaves the first 8 bytes free for storing the InternalType in the upper + * bits. + * + * PrimaryDiscriminator::pdListN - pdPath - Only has 3 available padding bits + * because: + * - tListN needs a size, whose lower bits we can't borrow. + * - tString and tPath have C-string fields, which don't necessarily need to + * be aligned. + * + * In this case we reserve their discriminators directly in the PrimaryDiscriminator + * bits stored in payload[0]. + * + * PrimaryDiscriminator::pdPairOfPointers - Payloads that consist of a pair of pointers. + * In this case the 3 lower bits of payload[1] can be tagged. + * + * The primary discriminator with value 0 is reserved for uninitialized Values, + * which are useful for diagnostics in C bindings. + */ + enum PrimaryDiscriminator : int { + pdUninitialized = 0, + pdSingleDWord, //< layout: Single/zero field payload + /* The order of these enumations must be the same as in InternalType. */ + pdListN, //< layout: Single untaggable field. + pdString, + pdPath, + pdPairOfPointers, //< layout: Pair of pointers payload + }; + + template + requires std::is_pointer_v + static T untagPointer(PackedPointer val) noexcept + { + return std::bit_cast(val & ~discriminatorMask); + } + + PrimaryDiscriminator getPrimaryDiscriminator() const noexcept + { + return static_cast(payload[0] & discriminatorMask); + } + + static void assertAligned(PackedPointer val) noexcept + { + assert((val & discriminatorMask) == 0 && "Pointer is not 8 bytes aligned"); + } + + template + void setSingleDWordPayload(PackedPointer untaggedVal) noexcept + { + /* There's plenty of free upper bits in the first dword, which is + used only for the discriminator. */ + payload[0] = static_cast(pdSingleDWord) | (static_cast(type) << discriminatorBits); + payload[1] = untaggedVal; + } + + template + void setUntaggablePayload(T * firstPtrField, U untaggableField) noexcept + { + static_assert(discriminator >= pdListN && discriminator <= pdPath); + auto firstFieldPayload = std::bit_cast(firstPtrField); + assertAligned(firstFieldPayload); + payload[0] = static_cast(discriminator) | firstFieldPayload; + payload[1] = std::bit_cast(untaggableField); + } + + template + void setPairOfPointersPayload(T * firstPtrField, U * secondPtrField) noexcept + { + static_assert(type >= tListSmall && type <= tLambda); + { + auto firstFieldPayload = std::bit_cast(firstPtrField); + assertAligned(firstFieldPayload); + payload[0] = static_cast(pdPairOfPointers) | firstFieldPayload; + } + { + auto secondFieldPayload = std::bit_cast(secondPtrField); + assertAligned(secondFieldPayload); + payload[1] = (type - tListSmall) | secondFieldPayload; + } + } + + template + requires std::is_pointer_v && std::is_pointer_v + void getPairOfPointersPayload(T & firstPtrField, U & secondPtrField) const noexcept + { + firstPtrField = untagPointer(payload[0]); + secondPtrField = untagPointer(payload[1]); + } + +protected: + /** Get internal type currently occupying the storage. */ + InternalType getInternalType() const noexcept + { + switch (auto pd = getPrimaryDiscriminator()) { + case pdUninitialized: + /* Discriminator value of zero is used to distinguish uninitialized values. */ + return tUninitialized; + case pdSingleDWord: + /* Payloads that only use up a single double word store the InternalType + in the upper bits of the first double word. */ + return InternalType(payload[0] >> discriminatorBits); + /* The order must match that of the enumerations defined in InternalType. */ + case pdListN: + case pdString: + case pdPath: + return static_cast(tListN + (pd - pdListN)); + case pdPairOfPointers: + return static_cast(tListSmall + (payload[1] & discriminatorMask)); + [[unlikely]] default: + unreachable(); + } + } + +#define NIX_VALUE_STORAGE_DEF_PAIR_OF_PTRS(TYPE, MEMBER_A, MEMBER_B) \ + \ + void getStorage(TYPE & val) const noexcept \ + { \ + getPairOfPointersPayload(val MEMBER_A, val MEMBER_B); \ + } \ + \ + void setStorage(TYPE val) noexcept \ + { \ + setPairOfPointersPayload>(val MEMBER_A, val MEMBER_B); \ + } + + NIX_VALUE_STORAGE_DEF_PAIR_OF_PTRS(SmallList, [0], [1]) + NIX_VALUE_STORAGE_DEF_PAIR_OF_PTRS(PrimOpApplicationThunk, .left, .right) + NIX_VALUE_STORAGE_DEF_PAIR_OF_PTRS(FunctionApplicationThunk, .left, .right) + NIX_VALUE_STORAGE_DEF_PAIR_OF_PTRS(ClosureThunk, .env, .expr) + NIX_VALUE_STORAGE_DEF_PAIR_OF_PTRS(Lambda, .env, .fun) + +#undef NIX_VALUE_STORAGE_DEF_PAIR_OF_PTRS + + void getStorage(NixInt & integer) const noexcept + { + /* PackedPointerType -> int64_t here is well-formed, since the standard requires + this conversion to follow 2's complement rules. This is just a no-op. */ + integer = NixInt(payload[1]); + } + + void getStorage(bool & boolean) const noexcept + { + boolean = payload[1]; + } + + void getStorage(Null & null) const noexcept {} + + void getStorage(NixFloat & fpoint) const noexcept + { + fpoint = std::bit_cast(payload[1]); + } + + void getStorage(ExternalValueBase *& external) const noexcept + { + external = std::bit_cast(payload[1]); + } + + void getStorage(PrimOp *& primOp) const noexcept + { + primOp = std::bit_cast(payload[1]); + } + + void getStorage(Bindings *& attrs) const noexcept + { + attrs = std::bit_cast(payload[1]); + } + + void getStorage(List & list) const noexcept + { + list.elems = untagPointer(payload[0]); + list.size = payload[1]; + } + + void getStorage(StringWithContext & string) const noexcept + { + string.context = untagPointer(payload[0]); + string.c_str = std::bit_cast(payload[1]); + } + + void getStorage(Path & path) const noexcept + { + path.accessor = untagPointer(payload[0]); + path.path = std::bit_cast(payload[1]); + } + + void setStorage(NixInt integer) noexcept + { + setSingleDWordPayload(integer.value); + } + + void setStorage(bool boolean) noexcept + { + setSingleDWordPayload(boolean); + } + + void setStorage(Null path) noexcept + { + setSingleDWordPayload(0); + } + + void setStorage(NixFloat fpoint) noexcept + { + setSingleDWordPayload(std::bit_cast(fpoint)); + } + + void setStorage(ExternalValueBase * external) noexcept + { + setSingleDWordPayload(std::bit_cast(external)); + } + + void setStorage(PrimOp * primOp) noexcept + { + setSingleDWordPayload(std::bit_cast(primOp)); + } + + void setStorage(Bindings * bindings) noexcept + { + setSingleDWordPayload(std::bit_cast(bindings)); + } + + void setStorage(List list) noexcept + { + setUntaggablePayload(list.elems, list.size); + } + + void setStorage(StringWithContext string) noexcept + { + setUntaggablePayload(string.context, string.c_str); + } + + void setStorage(Path path) noexcept + { + setUntaggablePayload(path.accessor, path.path); + } +}; + +/** + * View into a list of Value * that is itself immutable. + * + * Since not all representations of ValueStorage can provide + * a pointer to a const array of Value * this proxy class either + * stores the small list inline or points to the big list. + */ +class ListView +{ + using SpanType = std::span; + using SmallList = detail::ValueBase::SmallList; + using List = detail::ValueBase::List; + + std::variant raw; + +public: + ListView(SmallList list) + : raw(list) + { + } + + ListView(List list) + : raw(list) + { + } + + Value * const * data() const & noexcept + { + return std::visit( + overloaded{ + [](const SmallList & list) { return list.data(); }, [](const List & list) { return list.elems; }}, + raw); + } + + std::size_t size() const noexcept + { + return std::visit( + overloaded{ + [](const SmallList & list) -> std::size_t { return list.back() == nullptr ? 1 : 2; }, + [](const List & list) -> std::size_t { return list.size; }}, + raw); + } + + Value * operator[](std::size_t i) const noexcept + { + return data()[i]; + } + + SpanType span() const & + { + return SpanType(data(), size()); + } + + /* Ensure that no dangling views can be created accidentally, as that + would lead to hard to diagnose bugs that only affect small lists. */ + SpanType span() && = delete; + Value * const * data() && noexcept = delete; + + /** + * Random-access iterator that only allows iterating over a constant range + * of mutable Value pointers. + * + * @note Not a pointer to minimize potential misuses and implicitly relying + * on the iterator being a pointer. + **/ + class iterator + { + public: + using value_type = Value *; + using pointer = const value_type *; + using reference = const value_type &; + using difference_type = std::ptrdiff_t; + using iterator_category = std::random_access_iterator_tag; + + private: + pointer ptr = nullptr; + + friend class ListView; + + iterator(pointer ptr) + : ptr(ptr) + { + } + + public: + iterator() = default; + + reference operator*() const + { + return *ptr; + } + + const value_type * operator->() const + { + return ptr; + } + + reference operator[](difference_type diff) const + { + return ptr[diff]; + } + + iterator & operator++() + { + ++ptr; + return *this; + } + + iterator operator++(int) + { + pointer tmp = ptr; + ++*this; + return iterator(tmp); + } + + iterator & operator--() + { + --ptr; + return *this; + } + + iterator operator--(int) + { + pointer tmp = ptr; + --*this; + return iterator(tmp); + } + + iterator & operator+=(difference_type diff) + { + ptr += diff; + return *this; + } + + iterator operator+(difference_type diff) const + { + return iterator(ptr + diff); + } + + friend iterator operator+(difference_type diff, const iterator & rhs) + { + return iterator(diff + rhs.ptr); + } + + iterator & operator-=(difference_type diff) + { + ptr -= diff; + return *this; + } + + iterator operator-(difference_type diff) const + { + return iterator(ptr - diff); + } + + difference_type operator-(const iterator & rhs) const + { + return ptr - rhs.ptr; + } + + std::strong_ordering operator<=>(const iterator & rhs) const = default; + }; + + using const_iterator = iterator; + + iterator begin() const & + { + return data(); + } + + iterator end() const & + { + return data() + size(); + } + + /* Ensure that no dangling iterators can be created accidentally, as that + would lead to hard to diagnose bugs that only affect small lists. */ + iterator begin() && = delete; + iterator end() && = delete; +}; + +static_assert(std::random_access_iterator); + +struct Value : public ValueStorage +{ + friend std::string showType(const Value & v); + + template + bool isa() const noexcept + { + return ((getInternalType() == discriminator) || ...); + } + + template + T getStorage() const noexcept + { + if (getInternalType() != detail::payloadTypeToInternalType) [[unlikely]] + unreachable(); + T out; + ValueStorage::getStorage(out); + return out; + } + +public: + + /** + * Never modify the backing `Value` object! + */ + static Value * toPtr(SymbolStr str) noexcept; + + void print(EvalState & state, std::ostream & str, PrintOptions options = PrintOptions{}); + + // Functions needed to distinguish the type + // These should be removed eventually, by putting the functionality that's + // needed by callers into methods of this type + + // type() == nThunk + inline bool isThunk() const + { + return isa(); + }; + inline bool isApp() const + { + return isa(); + }; + inline bool isBlackhole() const; + + // type() == nFunction + inline bool isLambda() const + { + return isa(); + }; + inline bool isPrimOp() const + { + return isa(); + }; + inline bool isPrimOpApp() const + { + return isa(); + }; + + /** + * Returns the normal type of a Value. This only returns nThunk if + * the Value hasn't been forceValue'd + * + * @param invalidIsThunk Instead of aborting an an invalid (probably + * 0, so uninitialized) internal type, return `nThunk`. + */ + inline ValueType type(bool invalidIsThunk = false) const + { + switch (getInternalType()) { + case tUninitialized: + break; + case tInt: + return nInt; + case tBool: + return nBool; + case tString: + return nString; + case tPath: + return nPath; + case tNull: + return nNull; + case tAttrs: + return nAttrs; + case tListSmall: + case tListN: + return nList; + case tLambda: + case tPrimOp: + case tPrimOpApp: + return nFunction; + case tExternal: + return nExternal; + case tFloat: + return nFloat; + case tThunk: + case tApp: + return nThunk; + } + if (invalidIsThunk) + return nThunk; + else + unreachable(); + } + + /** + * A value becomes valid when it is initialized. We don't use this + * in the evaluator; only in the bindings, where the slight extra + * cost is warranted because of inexperienced callers. + */ + inline bool isValid() const noexcept + { + return !isa(); + } + + inline void mkInt(NixInt::Inner n) noexcept + { + mkInt(NixInt{n}); + } + + inline void mkInt(NixInt n) noexcept + { + setStorage(NixInt{n}); + } + + inline void mkBool(bool b) noexcept + { + setStorage(b); + } + + inline void mkString(const char * s, const char ** context = 0) noexcept + { + setStorage(StringWithContext{.c_str = s, .context = context}); + } + + void mkString(std::string_view s); + + void mkString(std::string_view s, const NixStringContext & context); + + void mkStringMove(const char * s, const NixStringContext & context); + + void mkPath(const SourcePath & path); + void mkPath(std::string_view path); + + inline void mkPath(SourceAccessor * accessor, const char * path) noexcept + { + setStorage(Path{.accessor = accessor, .path = path}); + } + + inline void mkNull() noexcept + { + setStorage(Null{}); + } + + inline void mkAttrs(Bindings * a) noexcept + { + setStorage(a); + } + + Value & mkAttrs(BindingsBuilder & bindings); + + void mkList(const ListBuilder & builder) noexcept + { + if (builder.size == 1) + setStorage(std::array{builder.inlineElems[0], nullptr}); + else if (builder.size == 2) + setStorage(std::array{builder.inlineElems[0], builder.inlineElems[1]}); + else + setStorage(List{.size = builder.size, .elems = builder.elems}); + } + + inline void mkThunk(Env * e, Expr * ex) noexcept + { + setStorage(ClosureThunk{.env = e, .expr = ex}); + } + + inline void mkApp(Value * l, Value * r) noexcept + { + setStorage(FunctionApplicationThunk{.left = l, .right = r}); + } + + inline void mkLambda(Env * e, ExprLambda * f) noexcept + { + setStorage(Lambda{.env = e, .fun = f}); + } + + inline void mkBlackhole(); + + void mkPrimOp(PrimOp * p); + + inline void mkPrimOpApp(Value * l, Value * r) noexcept + { + setStorage(PrimOpApplicationThunk{.left = l, .right = r}); + } + + /** + * For a `tPrimOpApp` value, get the original `PrimOp` value. + */ + const PrimOp * primOpAppPrimOp() const; + + inline void mkExternal(ExternalValueBase * e) noexcept + { + setStorage(e); + } + + inline void mkFloat(NixFloat n) noexcept + { + setStorage(n); + } + + bool isList() const noexcept + { + return isa(); + } + + ListView listView() const noexcept + { + return isa() ? ListView(getStorage()) : ListView(getStorage()); + } + + size_t listSize() const noexcept + { + return isa() ? (getStorage()[1] == nullptr ? 1 : 2) : getStorage().size; + } + + PosIdx determinePos(const PosIdx pos) const; + + /** + * Check whether forcing this value requires a trivial amount of + * computation. In particular, function applications are + * non-trivial. + */ + bool isTrivial() const; + + SourcePath path() const + { + return SourcePath(ref(pathAccessor()->shared_from_this()), CanonPath(CanonPath::unchecked_t(), pathStr())); + } + + std::string_view string_view() const noexcept + { + return std::string_view(getStorage().c_str); + } + + const char * c_str() const noexcept + { + return getStorage().c_str; + } + + const char ** context() const noexcept + { + return getStorage().context; + } + + ExternalValueBase * external() const noexcept + { + return getStorage(); + } + + const Bindings * attrs() const noexcept + { + return getStorage(); + } + + const PrimOp * primOp() const noexcept + { + return getStorage(); + } + + bool boolean() const noexcept + { + return getStorage(); + } + + NixInt integer() const noexcept + { + return getStorage(); + } + + NixFloat fpoint() const noexcept + { + return getStorage(); + } + + Lambda lambda() const noexcept + { + return getStorage(); + } + + ClosureThunk thunk() const noexcept + { + return getStorage(); + } + + PrimOpApplicationThunk primOpApp() const noexcept + { + return getStorage(); + } + + FunctionApplicationThunk app() const noexcept + { + return getStorage(); + } + + const char * pathStr() const noexcept + { + return getStorage().path; + } + + SourceAccessor * pathAccessor() const noexcept + { + return getStorage().accessor; + } +}; + +extern ExprBlackHole eBlackHole; + +bool Value::isBlackhole() const +{ + return isThunk() && thunk().expr == (Expr *) &eBlackHole; +} + +void Value::mkBlackhole() +{ + mkThunk(nullptr, (Expr *) &eBlackHole); +} + +typedef std::vector> ValueVector; +typedef std::unordered_map< + Symbol, + Value *, + std::hash, + std::equal_to, + traceable_allocator>> + ValueMap; +typedef std::map, traceable_allocator>> + ValueVectorMap; + +/** + * A value allocated in traceable memory. + */ +typedef std::shared_ptr RootValue; + +RootValue allocRootValue(Value * v); + +void forceNoNullByte(std::string_view s, std::function = nullptr); +} diff --git a/src/libexpr/value/context.hh b/src/libexpr/include/nix/expr/value/context.hh similarity index 94% rename from src/libexpr/value/context.hh rename to src/libexpr/include/nix/expr/value/context.hh index d6791c6e49c..f2de184ea1f 100644 --- a/src/libexpr/value/context.hh +++ b/src/libexpr/include/nix/expr/value/context.hh @@ -1,9 +1,9 @@ #pragma once ///@file -#include "comparator.hh" -#include "derived-path.hh" -#include "variant-wrapper.hh" +#include "nix/util/comparator.hh" +#include "nix/store/derived-path.hh" +#include "nix/util/variant-wrapper.hh" #include diff --git a/src/libexpr/json-to-value.cc b/src/libexpr/json-to-value.cc index 9ac56541ab9..e38ac7db40c 100644 --- a/src/libexpr/json-to-value.cc +++ b/src/libexpr/json-to-value.cc @@ -1,6 +1,6 @@ -#include "json-to-value.hh" -#include "value.hh" -#include "eval.hh" +#include "nix/expr/json-to-value.hh" +#include "nix/expr/value.hh" +#include "nix/expr/eval.hh" #include #include @@ -50,6 +50,7 @@ class JSONSax : nlohmann::json_sax { public: void key(string_t & name, EvalState & state) { + forceNoNullByte(name); attrs.insert_or_assign(state.symbols.create(name), &value(state)); } }; @@ -122,6 +123,7 @@ class JSONSax : nlohmann::json_sax { bool string(string_t & val) override { + forceNoNullByte(val); rs->value(state).mkString(val); rs->add(); return true; diff --git a/src/libexpr/lexer-helpers.cc b/src/libexpr/lexer-helpers.cc index d9eeb73e269..927e3cc7324 100644 --- a/src/libexpr/lexer-helpers.cc +++ b/src/libexpr/lexer-helpers.cc @@ -1,6 +1,4 @@ -#include "lexer-tab.hh" #include "lexer-helpers.hh" -#include "parser-tab.hh" void nix::lexer::internal::initLoc(YYLTYPE * loc) { diff --git a/src/libexpr/lexer-helpers.hh b/src/libexpr/lexer-helpers.hh index caba6e18f48..225eb157a96 100644 --- a/src/libexpr/lexer-helpers.hh +++ b/src/libexpr/lexer-helpers.hh @@ -1,5 +1,13 @@ #pragma once +#include + +// including the generated headers twice leads to errors +#ifndef BISON_HEADER +# include "lexer-tab.hh" +# include "parser-tab.hh" +#endif + namespace nix::lexer::internal { void initLoc(YYLTYPE * loc); diff --git a/src/libexpr/lexer.l b/src/libexpr/lexer.l index a7e44cb7288..1005f9f7ea5 100644 --- a/src/libexpr/lexer.l +++ b/src/libexpr/lexer.l @@ -1,3 +1,4 @@ +%option 8bit %option reentrant bison-bridge bison-locations %option align %option noyywrap @@ -16,7 +17,7 @@ %top { #include "parser-tab.hh" // YYSTYPE -#include "parser-state.hh" +#include "nix/expr/parser-state.hh" } %{ @@ -24,8 +25,7 @@ #pragma clang diagnostic ignored "-Wunneeded-internal-declaration" #endif -#include "nixexpr.hh" -#include "parser-tab.hh" +#include "nix/expr/nixexpr.hh" #include "lexer-helpers.hh" namespace nix { @@ -41,16 +41,18 @@ namespace nix { // we make use of the fact that the parser receives a private copy of the input // string and can munge around in it. -static StringToken unescapeStr(SymbolTable & symbols, char * s, size_t length) +// getting the position is expensive and thus it is implemented lazily. +static StringToken unescapeStr(char * const s, size_t length, std::function && pos) { - char * result = s; + bool noNullByte = true; char * t = s; - char c; // the input string is terminated with *two* NULs, so we can safely take // *one* character after the one being checked against. - while ((c = *s++)) { + for (size_t i = 0; i < length; t++) { + char c = s[i++]; + noNullByte &= c != '\0'; if (c == '\\') { - c = *s++; + c = s[i++]; if (c == 'n') *t = '\n'; else if (c == 'r') *t = '\r'; else if (c == 't') *t = '\t'; @@ -59,12 +61,14 @@ static StringToken unescapeStr(SymbolTable & symbols, char * s, size_t length) else if (c == '\r') { /* Normalise CR and CR/LF into LF. */ *t = '\n'; - if (*s == '\n') s++; /* cr/lf */ + if (s[i] == '\n') i++; /* cr/lf */ } else *t = c; - t++; } - return {result, size_t(t - result)}; + if (!noNullByte) { + forceNoNullByte({s, size_t(t - s)}, std::move(pos)); + } + return {s, size_t(t - s)}; } static void requireExperimentalFeature(const ExperimentalFeature & feature, const Pos & pos) @@ -175,7 +179,7 @@ or { return OR_KW; } /* It is impossible to match strings ending with '$' with one regex because trailing contexts are only valid at the end of a rule. (A sane but undocumented limitation.) */ - yylval->str = unescapeStr(state->symbols, yytext, yyleng); + yylval->str = unescapeStr(yytext, yyleng, [&]() { return state->positions[CUR_POS]; }); return STR; } \$\{ { PUSH_STATE(DEFAULT); return DOLLAR_CURLY; } @@ -191,6 +195,7 @@ or { return OR_KW; } \'\'(\ *\n)? { PUSH_STATE(IND_STRING); return IND_STRING_OPEN; } ([^\$\']|\$[^\{\']|\'[^\'\$])+ { yylval->str = {yytext, (size_t) yyleng, true}; + forceNoNullByte(yylval->str, [&]() { return state->positions[CUR_POS]; }); return IND_STR; } \'\'\$ | @@ -203,7 +208,7 @@ or { return OR_KW; } return IND_STR; } \'\'\\{ANY} { - yylval->str = unescapeStr(state->symbols, yytext + 2, yyleng - 2); + yylval->str = unescapeStr(yytext + 2, yyleng - 2, [&]() { return state->positions[CUR_POS]; }); return IND_STR; } \$\{ { PUSH_STATE(DEFAULT); return DOLLAR_CURLY; } diff --git a/src/libexpr/meson.build b/src/libexpr/meson.build index 28318579e92..f5adafae031 100644 --- a/src/libexpr/meson.build +++ b/src/libexpr/meson.build @@ -4,8 +4,6 @@ project('nix-expr', 'cpp', 'cpp_std=c++2a', # TODO(Qyriad): increase the warning level 'warning_level=1', - 'debug=true', - 'optimization=2', 'errorlogs=true', # Please print logs for tests that fail ], meson_version : '>= 1.1', @@ -14,9 +12,10 @@ project('nix-expr', 'cpp', cxx = meson.get_compiler('cpp') -subdir('build-utils-meson/deps-lists') +subdir('nix-meson-build-support/deps-lists') -configdata = configuration_data() +configdata_pub = configuration_data() +configdata_priv = configuration_data() deps_private_maybe_subproject = [ ] @@ -25,7 +24,18 @@ deps_public_maybe_subproject = [ dependency('nix-store'), dependency('nix-fetchers'), ] -subdir('build-utils-meson/subprojects') +subdir('nix-meson-build-support/subprojects') +subdir('nix-meson-build-support/big-objs') + +# Check for each of these functions, and create a define like `#define HAVE_LCHOWN 1`. +check_funcs = [ + 'sysconf', +] +foreach funcspec : check_funcs + define_name = 'HAVE_' + funcspec.underscorify().to_upper() + define_value = cxx.has_function(funcspec).to_int() + configdata_priv.set(define_name, define_value) +endforeach boost = dependency( 'boost', @@ -48,11 +58,13 @@ if bdw_gc.found() ] define_name = 'HAVE_' + funcspec.underscorify().to_upper() define_value = cxx.has_function(funcspec).to_int() - configdata.set(define_name, define_value) + configdata_priv.set(define_name, define_value) endforeach - configdata.set('GC_THREADS', 1) + # Affects ABI, because it changes what bdw_gc itself does! + configdata_pub.set('GC_THREADS', 1) endif -configdata.set('HAVE_BOEHMGC', bdw_gc.found().to_int()) +# Used in public header. Affects ABI! +configdata_pub.set('NIX_USE_BOEHMGC', bdw_gc.found().to_int()) toml11 = dependency( 'toml11', @@ -62,22 +74,12 @@ toml11 = dependency( ) deps_other += toml11 -config_h = configure_file( - configuration : configdata, - output : 'config-expr.hh', +config_priv_h = configure_file( + configuration : configdata_priv, + output : 'expr-config-private.hh', ) -add_project_arguments( - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. - '-include', 'config-util.hh', - '-include', 'config-store.hh', - # '-include', 'config-fetchers.h', - '-include', 'config-expr.hh', - language : 'cpp', -) - -subdir('build-utils-meson/common') +subdir('nix-meson-build-support/common') parser_tab = custom_target( input : 'parser.y', @@ -110,6 +112,7 @@ lexer_tab = custom_target( ], command : [ 'flex', + '-Cf', # Use full scanner tables '--outfile', '@OUTPUT0@', '--header-file=' + '@OUTPUT1@', @@ -121,13 +124,12 @@ lexer_tab = custom_target( install_dir : get_option('includedir') / 'nix', ) -subdir('build-utils-meson/generate-header') +subdir('nix-meson-build-support/generate-header') generated_headers = [] foreach header : [ 'imported-drv-to-derivation.nix', 'fetchurl.nix', - 'call-flake.nix', ] generated_headers += gen_header.process(header) endforeach @@ -138,6 +140,8 @@ sources = files( 'eval-cache.cc', 'eval-error.cc', 'eval-gc.cc', + 'eval-profiler-settings.cc', + 'eval-profiler.cc', 'eval-settings.cc', 'eval.cc', 'function-trace.cc', @@ -155,54 +159,29 @@ sources = files( 'value/context.cc', ) -include_dirs = [include_directories('.')] - -headers = [config_h] + files( - 'attr-path.hh', - 'attr-set.hh', - 'eval-cache.hh', - 'eval-error.hh', - 'eval-gc.hh', - 'eval-inline.hh', - 'eval-settings.hh', - 'eval.hh', - 'function-trace.hh', - 'gc-small-vector.hh', - 'get-drvs.hh', - 'json-to-value.hh', - # internal: 'lexer-helpers.hh', - 'nixexpr.hh', - 'parser-state.hh', - 'pos-idx.hh', - 'pos-table.hh', - 'primops.hh', - 'print-ambiguous.hh', - 'print-options.hh', - 'print.hh', - 'repl-exit-status.hh', - 'search-path.hh', - 'symbol-table.hh', - 'value-to-json.hh', - 'value-to-xml.hh', - 'value.hh', - 'value/context.hh', -) +subdir('include/nix/expr') subdir('primops') +subdir('nix-meson-build-support/export-all-symbols') +subdir('nix-meson-build-support/windows-version') + this_library = library( 'nixexpr', sources, + config_priv_h, parser_tab, lexer_tab, generated_headers, dependencies : deps_public + deps_private + deps_other, + include_directories : include_dirs, + link_args: linker_export_flags, prelink : true, # For C++ static initializers install : true, ) -install_headers(headers, subdir : 'nix', preserve_path : true) +install_headers(headers, subdir : 'nix/expr', preserve_path : true) libraries_private = [] -subdir('build-utils-meson/export') +subdir('nix-meson-build-support/export') diff --git a/src/libexpr/nix-meson-build-support b/src/libexpr/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/libexpr/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/libexpr/nixexpr.cc b/src/libexpr/nixexpr.cc index 063ff07537b..92071b22d39 100644 --- a/src/libexpr/nixexpr.cc +++ b/src/libexpr/nixexpr.cc @@ -1,13 +1,13 @@ -#include "nixexpr.hh" -#include "eval.hh" -#include "symbol-table.hh" -#include "util.hh" -#include "print.hh" +#include "nix/expr/nixexpr.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/symbol-table.hh" +#include "nix/util/util.hh" +#include "nix/expr/print.hh" #include #include -#include "strings-inline.hh" +#include "nix/util/strings-inline.hh" namespace nix { @@ -310,7 +310,7 @@ void ExprVar::bindVars(EvalState & es, const std::shared_ptr & const StaticEnv * curEnv; Level level; int withLevel = -1; - for (curEnv = env.get(), level = 0; curEnv; curEnv = curEnv->up, level++) { + for (curEnv = env.get(), level = 0; curEnv; curEnv = curEnv->up.get(), level++) { if (curEnv->isWith) { if (withLevel == -1) withLevel = level; } else { @@ -331,7 +331,7 @@ void ExprVar::bindVars(EvalState & es, const std::shared_ptr & "undefined variable '%1%'", es.symbols[name] ).atPos(pos).debugThrow(); - for (auto * e = env.get(); e && !fromWith; e = e->up) + for (auto * e = env.get(); e && !fromWith; e = e->up.get()) fromWith = e->isWith; this->level = withLevel; } @@ -379,7 +379,7 @@ std::shared_ptr ExprAttrs::bindInheritSources( // and displacement, and nothing else is allowed to access it. ideally we'd // not even *have* an expr that grabs anything from this env since it's fully // invisible, but the evaluator does not allow for this yet. - auto inner = std::make_shared(nullptr, env.get(), 0); + auto inner = std::make_shared(nullptr, env, 0); for (auto from : *inheritFromExprs) from->bindVars(es, env); @@ -393,7 +393,7 @@ void ExprAttrs::bindVars(EvalState & es, const std::shared_ptr if (recursive) { auto newEnv = [&] () -> std::shared_ptr { - auto newEnv = std::make_shared(nullptr, env.get(), attrs.size()); + auto newEnv = std::make_shared(nullptr, env, attrs.size()); Displacement displ = 0; for (auto & i : attrs) @@ -440,7 +440,7 @@ void ExprLambda::bindVars(EvalState & es, const std::shared_ptr es.exprEnvs.insert(std::make_pair(this, env)); auto newEnv = std::make_shared( - nullptr, env.get(), + nullptr, env, (hasFormals() ? formals->formals.size() : 0) + (!arg ? 0 : 1)); @@ -474,7 +474,7 @@ void ExprCall::bindVars(EvalState & es, const std::shared_ptr & void ExprLet::bindVars(EvalState & es, const std::shared_ptr & env) { auto newEnv = [&] () -> std::shared_ptr { - auto newEnv = std::make_shared(nullptr, env.get(), attrs->attrs.size()); + auto newEnv = std::make_shared(nullptr, env, attrs->attrs.size()); Displacement displ = 0; for (auto & i : attrs->attrs) @@ -500,7 +500,7 @@ void ExprWith::bindVars(EvalState & es, const std::shared_ptr & es.exprEnvs.insert(std::make_pair(this, env)); parentWith = nullptr; - for (auto * e = env.get(); e && !parentWith; e = e->up) + for (auto * e = env.get(); e && !parentWith; e = e->up.get()) parentWith = e->isWith; /* Does this `with' have an enclosing `with'? If so, record its @@ -509,14 +509,14 @@ void ExprWith::bindVars(EvalState & es, const std::shared_ptr & const StaticEnv * curEnv; Level level; prevWith = 0; - for (curEnv = env.get(), level = 1; curEnv; curEnv = curEnv->up, level++) + for (curEnv = env.get(), level = 1; curEnv; curEnv = curEnv->up.get(), level++) if (curEnv->isWith) { prevWith = level; break; } attrs->bindVars(es, env); - auto newEnv = std::make_shared(this, env.get()); + auto newEnv = std::make_shared(this, env); body->bindVars(es, newEnv); } @@ -601,47 +601,12 @@ void ExprLambda::setDocComment(DocComment docComment) { } }; - - -/* Position table. */ - -Pos PosTable::operator[](PosIdx p) const -{ - auto origin = resolve(p); - if (!origin) - return {}; - - const auto offset = origin->offsetOf(p); - - Pos result{0, 0, origin->origin}; - auto lines = this->lines.lock(); - auto linesForInput = (*lines)[origin->offset]; - - if (linesForInput.empty()) { - auto source = result.getSource().value_or(""); - const char * begin = source.data(); - for (Pos::LinesIterator it(source), end; it != end; it++) - linesForInput.push_back(it->data() - begin); - if (linesForInput.empty()) - linesForInput.push_back(0); - } - // as above: the first line starts at byte 0 and is always present - auto lineStartOffset = std::prev( - std::upper_bound(linesForInput.begin(), linesForInput.end(), offset)); - - result.line = 1 + (lineStartOffset - linesForInput.begin()); - result.column = 1 + (offset - *lineStartOffset); - return result; -} - - - /* Symbol table. */ size_t SymbolTable::totalSize() const { size_t n = 0; - dump([&] (const std::string & s) { n += s.size(); }); + dump([&] (SymbolStr s) { n += s.size(); }); return n; } diff --git a/src/libexpr/package.nix b/src/libexpr/package.nix index d97e7f3a82f..50161c58ba2 100644 --- a/src/libexpr/package.nix +++ b/src/libexpr/package.nix @@ -1,33 +1,34 @@ -{ lib -, stdenv -, mkMesonLibrary - -, bison -, flex -, cmake # for resolving toml11 dep - -, nix-util -, nix-store -, nix-fetchers -, boost -, boehmgc -, nlohmann_json -, toml11 - -# Configuration Options - -, version - -# Whether to use garbage collection for the Nix language evaluator. -# -# If it is disabled, we just leak memory, but this is not as bad as it -# sounds so long as evaluation just takes places within short-lived -# processes. (When the process exits, the memory is reclaimed; it is -# only leaked *within* the process.) -# -# Temporarily disabled on Windows because the `GC_throw_bad_alloc` -# symbol is missing during linking. -, enableGC ? !stdenv.hostPlatform.isWindows +{ + lib, + stdenv, + mkMesonLibrary, + + bison, + flex, + cmake, # for resolving toml11 dep + + nix-util, + nix-store, + nix-fetchers, + boost, + boehmgc, + nlohmann_json, + toml11, + + # Configuration Options + + version, + + # Whether to use garbage collection for the Nix language evaluator. + # + # If it is disabled, we just leak memory, but this is not as bad as it + # sounds so long as evaluation just takes places within short-lived + # processes. (When the process exits, the memory is reclaimed; it is + # only leaked *within* the process.) + # + # Temporarily disabled on Windows because the `GC_throw_bad_alloc` + # symbol is missing during linking. + enableGC ? !stdenv.hostPlatform.isWindows, }: let @@ -40,21 +41,19 @@ mkMesonLibrary (finalAttrs: { workDir = ./.; fileset = fileset.unions [ - ../../build-utils-meson - ./build-utils-meson + ../../nix-meson-build-support + ./nix-meson-build-support ../../.version ./.version ./meson.build ./meson.options ./primops/meson.build + ./include/nix/expr/meson.build (fileset.fileFilter (file: file.hasExt "cc") ./.) (fileset.fileFilter (file: file.hasExt "hh") ./.) ./lexer.l ./parser.y - (fileset.difference - (fileset.fileFilter (file: file.hasExt "nix") ./.) - ./package.nix - ) + (fileset.difference (fileset.fileFilter (file: file.hasExt "nix") ./.) ./package.nix) ]; nativeBuildInputs = [ @@ -79,27 +78,10 @@ mkMesonLibrary (finalAttrs: { nlohmann_json ] ++ lib.optional enableGC boehmgc; - preConfigure = - # "Inline" .version so it's not a symlink, and includes the suffix. - # Do the meson utils, without modification. - '' - chmod u+w ./.version - echo ${version} > ../../.version - ''; - mesonFlags = [ (lib.mesonEnable "gc" enableGC) ]; - env = { - # Needed for Meson to find Boost. - # https://github.com/NixOS/nixpkgs/issues/86131. - BOOST_INCLUDEDIR = "${lib.getDev boost}/include"; - BOOST_LIBRARYDIR = "${lib.getLib boost}/lib"; - } // lib.optionalAttrs (stdenv.isLinux && !(stdenv.hostPlatform.isStatic && stdenv.system == "aarch64-linux")) { - LDFLAGS = "-fuse-ld=gold"; - }; - meta = { platforms = lib.platforms.unix ++ lib.platforms.windows; }; diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y index 944c7b1af31..8878b86c290 100644 --- a/src/libexpr/parser.y +++ b/src/libexpr/parser.y @@ -17,14 +17,14 @@ #include -#include "finally.hh" -#include "util.hh" -#include "users.hh" +#include "nix/util/finally.hh" +#include "nix/util/util.hh" +#include "nix/util/users.hh" -#include "nixexpr.hh" -#include "eval.hh" -#include "eval-settings.hh" -#include "parser-state.hh" +#include "nix/expr/nixexpr.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/eval-settings.hh" +#include "nix/expr/parser-state.hh" // Bison seems to have difficulty growing the parser stack when using C++ with // a custom location type. This undocumented macro tells Bison that our @@ -179,7 +179,12 @@ static Expr * makeCall(PosIdx pos, Expr * fn, Expr * arg) { %% -start: expr { state->result = $1; }; +start: expr { + state->result = $1; + + // This parser does not use yynerrs; suppress the warning. + (void) yynerrs; +}; expr: expr_function; @@ -359,11 +364,18 @@ string_parts_interpolated path_start : PATH { - Path path(absPath(std::string_view{$1.p, $1.l}, state->basePath.path.abs())); + std::string_view literal({$1.p, $1.l}); + Path path(absPath(literal, state->basePath.path.abs())); /* add back in the trailing '/' to the first segment */ - if ($1.p[$1.l-1] == '/' && $1.l > 1) - path += "/"; - $$ = new ExprPath(ref(state->rootFS), std::move(path)); + if (literal.size() > 1 && literal.back() == '/') + path += '/'; + $$ = + /* Absolute paths are always interpreted relative to the + root filesystem accessor, rather than the accessor of the + current Nix expression. */ + literal.front() == '/' + ? new ExprPath(state->rootFS, std::move(path)) + : new ExprPath(state->basePath.accessor, std::move(path)); } | HPATH { if (state->settings.pureEval) { @@ -507,7 +519,7 @@ formal %% -#include "eval.hh" +#include "nix/expr/eval.hh" namespace nix { diff --git a/src/libexpr/paths.cc b/src/libexpr/paths.cc index 50d0d989564..c5107de3a5e 100644 --- a/src/libexpr/paths.cc +++ b/src/libexpr/paths.cc @@ -1,4 +1,5 @@ -#include "eval.hh" +#include "nix/store/store-api.hh" +#include "nix/expr/eval.hh" namespace nix { @@ -12,4 +13,9 @@ SourcePath EvalState::rootPath(PathView path) return {rootFS, CanonPath(absPath(path))}; } +SourcePath EvalState::storePath(const StorePath & path) +{ + return {rootFS, CanonPath{store->printStorePath(path)}}; +} + } diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 7e13e945cf2..99ca19d7e60 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -1,19 +1,20 @@ -#include "derivations.hh" -#include "downstream-placeholder.hh" -#include "eval-inline.hh" -#include "eval.hh" -#include "eval-settings.hh" -#include "gc-small-vector.hh" -#include "json-to-value.hh" -#include "names.hh" -#include "path-references.hh" -#include "store-api.hh" -#include "util.hh" -#include "processes.hh" -#include "value-to-json.hh" -#include "value-to-xml.hh" -#include "primops.hh" -#include "fetch-to-store.hh" +#include "nix/store/derivations.hh" +#include "nix/store/downstream-placeholder.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/eval-settings.hh" +#include "nix/expr/gc-small-vector.hh" +#include "nix/expr/json-to-value.hh" +#include "nix/store/names.hh" +#include "nix/store/path-references.hh" +#include "nix/store/store-api.hh" +#include "nix/util/util.hh" +#include "nix/util/processes.hh" +#include "nix/expr/value-to-json.hh" +#include "nix/expr/value-to-xml.hh" +#include "nix/expr/primops.hh" +#include "nix/fetchers/fetch-to-store.hh" +#include "nix/util/sort.hh" #include #include @@ -47,6 +48,15 @@ static inline Value * mkString(EvalState & state, const std::csub_match & match) return v; } +std::string EvalState::realiseString(Value & s, StorePathSet * storePathsOutMaybe, bool isIFD, const PosIdx pos) +{ + nix::NixStringContext stringContext; + auto rawStr = coerceToString(pos, s, stringContext, "while realising a string").toOwned(); + auto rewrites = realiseContext(stringContext, storePathsOutMaybe, isIFD); + + return nix::rewriteStrings(rawStr, rewrites); +} + StringMap EvalState::realiseContext(const NixStringContext & context, StorePathSet * maybePathsOut, bool isIFD) { std::vector drvs; @@ -81,11 +91,19 @@ StringMap EvalState::realiseContext(const NixStringContext & context, StorePathS if (drvs.empty()) return {}; - if (isIFD && !settings.enableImportFromDerivation) - error( - "cannot build '%1%' during evaluation because the option 'allow-import-from-derivation' is disabled", - drvs.begin()->to_string(*store) - ).debugThrow(); + if (isIFD) { + if (!settings.enableImportFromDerivation) + error( + "cannot build '%1%' during evaluation because the option 'allow-import-from-derivation' is disabled", + drvs.begin()->to_string(*store) + ).debugThrow(); + + if (settings.traceImportFromDerivation) + warn( + "built '%1%' during evaluation due to an import from derivation", + drvs.begin()->to_string(*store) + ); + } /* Build/substitute the context. */ std::vector buildReqs; @@ -119,11 +137,9 @@ StringMap EvalState::realiseContext(const NixStringContext & context, StorePathS if (store != buildStore) copyClosure(*buildStore, *store, outputsToCopyAndAllow); if (isIFD) { - for (auto & outputPath : outputsToCopyAndAllow) { - /* Add the output of this derivations to the allowed - paths. */ - allowPath(outputPath); - } + /* Allow access to the output closures of this derivation. */ + for (auto & outputPath : outputsToCopyAndAllow) + allowClosure(outputPath); } return res; @@ -138,8 +154,7 @@ static SourcePath realisePath(EvalState & state, const PosIdx pos, Value & v, st try { if (!context.empty() && path.accessor == state.rootFS) { auto rewrites = state.realiseContext(context); - auto realPath = state.toRealPath(rewriteStrings(path.path.abs(), rewrites), context); - path = {path.accessor, CanonPath(realPath)}; + path = {path.accessor, CanonPath(rewriteStrings(path.path.abs(), rewrites))}; } return resolveSymlinks ? path.resolveSymlinks(*resolveSymlinks) : path; } catch (Error & e) { @@ -232,7 +247,7 @@ static void scopedImport(EvalState & state, const PosIdx pos, SourcePath & path, Env * env = &state.allocEnv(vScope->attrs()->size()); env->up = &state.baseEnv; - auto staticEnv = std::make_shared(nullptr, state.staticBaseEnv.get(), vScope->attrs()->size()); + auto staticEnv = std::make_shared(nullptr, state.staticBaseEnv, vScope->attrs()->size()); unsigned int displ = 0; for (auto & attr : *vScope->attrs()) { @@ -331,7 +346,7 @@ static RegisterPrimOp primop_import({ > } > ``` > - > then the following `foo.nix` will give an error: + > then the following `foo.nix` throws an error: > > ```nix > # foo.nix @@ -400,7 +415,7 @@ void prim_importNative(EvalState & state, const PosIdx pos, Value * * args, Valu void prim_exec(EvalState & state, const PosIdx pos, Value * * args, Value & v) { state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.exec"); - auto elems = args[0]->listElems(); + auto elems = args[0]->listView(); auto count = args[0]->listSize(); if (count == 0) state.error("at least one argument to 'exec' required").atPos(pos).debugThrow(); @@ -409,7 +424,7 @@ void prim_exec(EvalState & state, const PosIdx pos, Value * * args, Value & v) "while evaluating the first element of the argument passed to builtins.exec", false, false).toOwned(); Strings commandArgs; - for (unsigned int i = 1; i < args[0]->listSize(); ++i) { + for (size_t i = 1; i < count; ++i) { commandArgs.push_back( state.coerceToString(pos, *elems[i], context, "while evaluating an element of the argument passed to builtins.exec", @@ -637,7 +652,7 @@ struct CompareValues // Note: we don't take the accessor into account // since it's not obvious how to compare them in a // reproducible way. - return strcmp(v1->payload.path.path, v2->payload.path.path) < 0; + return strcmp(v1->pathStr(), v2->pathStr()) < 0; case nList: // Lexicographic comparison for (size_t i = 0;; i++) { @@ -645,8 +660,8 @@ struct CompareValues return false; } else if (i == v1->listSize()) { return true; - } else if (!state.eqValues(*v1->listElems()[i], *v2->listElems()[i], pos, errorCtx)) { - return (*this)(v1->listElems()[i], v2->listElems()[i], "while comparing two list elements"); + } else if (!state.eqValues(*v1->listView()[i], *v2->listView()[i], pos, errorCtx)) { + return (*this)(v1->listView()[i], v2->listView()[i], "while comparing two list elements"); } } default: @@ -664,31 +679,17 @@ struct CompareValues typedef std::list> ValueList; - -static Bindings::const_iterator getAttr( - EvalState & state, - Symbol attrSym, - const Bindings * attrSet, - std::string_view errorCtx) -{ - auto value = attrSet->find(attrSym); - if (value == attrSet->end()) { - state.error("attribute '%s' missing", state.symbols[attrSym]).withTrace(noPos, errorCtx).debugThrow(); - } - return value; -} - static void prim_genericClosure(EvalState & state, const PosIdx pos, Value * * args, Value & v) { state.forceAttrs(*args[0], noPos, "while evaluating the first argument passed to builtins.genericClosure"); /* Get the start set. */ - auto startSet = getAttr(state, state.sStartSet, args[0]->attrs(), "in the attrset passed as argument to builtins.genericClosure"); + auto startSet = state.getAttr(state.sStartSet, args[0]->attrs(), "in the attrset passed as argument to builtins.genericClosure"); state.forceList(*startSet->value, noPos, "while evaluating the 'startSet' attribute passed as argument to builtins.genericClosure"); ValueList workSet; - for (auto elem : startSet->value->listItems()) + for (auto elem : startSet->value->listView()) workSet.push_back(elem); if (startSet->value->listSize() == 0) { @@ -697,7 +698,7 @@ static void prim_genericClosure(EvalState & state, const PosIdx pos, Value * * a } /* Get the operator. */ - auto op = getAttr(state, state.sOperator, args[0]->attrs(), "in the attrset passed as argument to builtins.genericClosure"); + auto op = state.getAttr(state.sOperator, args[0]->attrs(), "in the attrset passed as argument to builtins.genericClosure"); state.forceFunction(*op->value, noPos, "while evaluating the 'operator' attribute passed as argument to builtins.genericClosure"); /* Construct the closure by applying the operator to elements of @@ -714,7 +715,7 @@ static void prim_genericClosure(EvalState & state, const PosIdx pos, Value * * a state.forceAttrs(*e, noPos, "while evaluating one of the elements generated by (or initially passed to) builtins.genericClosure"); - auto key = getAttr(state, state.sKey, e->attrs(), "in one of the attrsets generated by (or initially passed to) builtins.genericClosure"); + auto key = state.getAttr(state.sKey, e->attrs(), "in one of the attrsets generated by (or initially passed to) builtins.genericClosure"); state.forceValue(*key->value, noPos); if (!doneKeys.insert(key->value).second) continue; @@ -726,7 +727,7 @@ static void prim_genericClosure(EvalState & state, const PosIdx pos, Value * * a state.forceList(newElements, noPos, "while evaluating the return value of the `operator` passed to builtins.genericClosure"); /* Add the values returned by the operator to the work set. */ - for (auto elem : newElements.listItems()) { + for (auto elem : newElements.listView()) { state.forceValue(*elem, noPos); // "while evaluating one one of the elements returned by the `operator` passed to builtins.genericClosure"); workSet.push_back(elem); } @@ -882,18 +883,40 @@ static void prim_ceil(EvalState & state, const PosIdx pos, Value * * args, Value { auto value = state.forceFloat(*args[0], args[0]->determinePos(pos), "while evaluating the first argument passed to builtins.ceil"); - v.mkInt(ceil(value)); + auto ceilValue = ceil(value); + bool isInt = args[0]->type() == nInt; + constexpr NixFloat int_min = std::numeric_limits::min(); // power of 2, so that no rounding occurs + if (ceilValue >= int_min && ceilValue < -int_min) { + v.mkInt(ceilValue); + } else if (isInt) { + // a NixInt, e.g. INT64_MAX, can be rounded to -int_min due to the cast to NixFloat + state.error("Due to a bug (see https://github.com/NixOS/nix/issues/12899) the NixInt argument %1% caused undefined behavior in previous Nix versions.\n\tFuture Nix versions might implement the correct behavior.", args[0]->integer().value).atPos(pos).debugThrow(); + } else { + state.error("NixFloat argument %1% is not in the range of NixInt", args[0]->fpoint()).atPos(pos).debugThrow(); + } + // `forceFloat` casts NixInt to NixFloat, but instead NixInt args shall be returned unmodified + if (isInt) { + auto arg = args[0]->integer(); + auto res = v.integer(); + if (arg != res) { + state.error("Due to a bug (see https://github.com/NixOS/nix/issues/12899) a loss of precision occurred in previous Nix versions because the NixInt argument %1% was rounded to %2%.\n\tFuture Nix versions might implement the correct behavior.", arg, res).atPos(pos).debugThrow(); + } + } } static RegisterPrimOp primop_ceil({ .name = "__ceil", - .args = {"double"}, + .args = {"number"}, .doc = R"( - Converts an IEEE-754 double-precision floating-point number (*double*) to - the next higher integer. + Rounds and converts *number* to the next higher NixInt value if possible, i.e. `ceil *number* >= *number*` and + `ceil *number* - *number* < 1`. - If the datatype is neither an integer nor a "float", an evaluation error will be - thrown. + An evaluation error is thrown, if there exists no such NixInt value `ceil *number*`. + Due to bugs in previous Nix versions an evaluation error might be thrown, if the datatype of *number* is + a NixInt and if `*number* < -9007199254740992` or `*number* > 9007199254740992`. + + If the datatype of *number* is neither a NixInt (signed 64-bit integer) nor a NixFloat + (IEEE-754 double-precision floating-point number), an evaluation error is thrown. )", .fun = prim_ceil, }); @@ -901,18 +924,40 @@ static RegisterPrimOp primop_ceil({ static void prim_floor(EvalState & state, const PosIdx pos, Value * * args, Value & v) { auto value = state.forceFloat(*args[0], args[0]->determinePos(pos), "while evaluating the first argument passed to builtins.floor"); - v.mkInt(floor(value)); + auto floorValue = floor(value); + bool isInt = args[0]->type() == nInt; + constexpr NixFloat int_min = std::numeric_limits::min(); // power of 2, so that no rounding occurs + if (floorValue >= int_min && floorValue < -int_min) { + v.mkInt(floorValue); + } else if (isInt) { + // a NixInt, e.g. INT64_MAX, can be rounded to -int_min due to the cast to NixFloat + state.error("Due to a bug (see https://github.com/NixOS/nix/issues/12899) the NixInt argument %1% caused undefined behavior in previous Nix versions.\n\tFuture Nix versions might implement the correct behavior.", args[0]->integer().value).atPos(pos).debugThrow(); + } else { + state.error("NixFloat argument %1% is not in the range of NixInt", args[0]->fpoint()).atPos(pos).debugThrow(); + } + // `forceFloat` casts NixInt to NixFloat, but instead NixInt args shall be returned unmodified + if (isInt) { + auto arg = args[0]->integer(); + auto res = v.integer(); + if (arg != res) { + state.error("Due to a bug (see https://github.com/NixOS/nix/issues/12899) a loss of precision occurred in previous Nix versions because the NixInt argument %1% was rounded to %2%.\n\tFuture Nix versions might implement the correct behavior.", arg, res).atPos(pos).debugThrow(); + } + } } static RegisterPrimOp primop_floor({ .name = "__floor", - .args = {"double"}, + .args = {"number"}, .doc = R"( - Converts an IEEE-754 double-precision floating-point number (*double*) to - the next lower integer. + Rounds and converts *number* to the next lower NixInt value if possible, i.e. `floor *number* <= *number*` and + `*number* - floor *number* < 1`. - If the datatype is neither an integer nor a "float", an evaluation error will be - thrown. + An evaluation error is thrown, if there exists no such NixInt value `floor *number*`. + Due to bugs in previous Nix versions an evaluation error might be thrown, if the datatype of *number* is + a NixInt and if `*number* < -9007199254740992` or `*number* > 9007199254740992`. + + If the datatype of *number* is neither a NixInt (signed 64-bit integer) nor a NixFloat + (IEEE-754 double-precision floating-point number), an evaluation error will be thrown. )", .fun = prim_floor, }); @@ -929,7 +974,7 @@ static void prim_tryEval(EvalState & state, const PosIdx pos, Value * * args, Va ReplExitStatus (* savedDebugRepl)(ref es, const ValMap & extraEnv) = nullptr; if (state.debugRepl && state.settings.ignoreExceptionsDuringTry) { - /* to prevent starting the repl from exceptions withing a tryEval, null it. */ + /* to prevent starting the repl from exceptions within a tryEval, null it. */ savedDebugRepl = state.debugRepl; state.debugRepl = nullptr; } @@ -958,15 +1003,15 @@ static RegisterPrimOp primop_tryEval({ Try to shallowly evaluate *e*. Return a set containing the attributes `success` (`true` if *e* evaluated successfully, `false` if an error was thrown) and `value`, equalling *e* if - successful and `false` otherwise. `tryEval` will only prevent + successful and `false` otherwise. `tryEval` only prevents errors created by `throw` or `assert` from being thrown. - Errors `tryEval` will not catch are for example those created + Errors `tryEval` doesn't catch are, for example, those created by `abort` and type errors generated by builtins. Also note that this doesn't evaluate *e* deeply, so `let e = { x = throw ""; }; - in (builtins.tryEval e).success` will be `true`. Using + in (builtins.tryEval e).success` is `true`. Using `builtins.deepSeq` one can get the expected result: `let e = { x = throw ""; }; in - (builtins.tryEval (builtins.deepSeq e e)).success` will be + (builtins.tryEval (builtins.deepSeq e e)).success` is `false`. `tryEval` intentionally does not return the error message, because that risks bringing non-determinism into the evaluation result, and it would become very difficult to improve error reporting without breaking existing expressions. @@ -1064,7 +1109,7 @@ static RegisterPrimOp primop_trace({ If the [`debugger-on-trace`](@docroot@/command-ref/conf-file.md#conf-debugger-on-trace) option is set to `true` and the `--debugger` flag is given, the - interactive debugger will be started when `trace` is called (like + interactive debugger is started when `trace` is called (like [`break`](@docroot@/language/builtins.md#builtins-break)). )", .fun = prim_trace, @@ -1100,7 +1145,7 @@ static RegisterPrimOp primop_warn({ .name = "__warn", .args = {"e1", "e2"}, .doc = R"( - Evaluate *e1*, which must be a string and print iton standard error as a warning. + Evaluate *e1*, which must be a string, and print it on standard error as a warning. Then return *e2*. This function is useful for non-critical situations where attention is advisable. @@ -1113,7 +1158,7 @@ static RegisterPrimOp primop_warn({ If the [`abort-on-warn`](@docroot@/command-ref/conf-file.md#conf-abort-on-warn) - option is set, the evaluation will be aborted after the warning is printed. + option is set, the evaluation is aborted after the warning is printed. This is useful to reveal the stack trace of the warning, when the context is non-interactive and a debugger can not be launched. )", .fun = prim_warn, @@ -1135,7 +1180,7 @@ static void prim_second(EvalState & state, const PosIdx pos, Value * * args, Val static void derivationStrictInternal( EvalState & state, - const std::string & name, + std::string_view name, const Bindings * attrs, Value & v); @@ -1153,9 +1198,9 @@ static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * * auto attrs = args[0]->attrs(); /* Figure out the name first (for stack backtraces). */ - auto nameAttr = getAttr(state, state.sName, attrs, "in the attrset passed as argument to builtins.derivationStrict"); + auto nameAttr = state.getAttr(state.sName, attrs, "in the attrset passed as argument to builtins.derivationStrict"); - std::string drvName; + std::string_view drvName; try { drvName = state.forceStringNoCtx(*nameAttr->value, pos, "while evaluating the `name` attribute passed to builtins.derivationStrict"); } catch (Error & e) { @@ -1214,7 +1259,7 @@ static void checkDerivationName(EvalState & state, std::string_view drvName) static void derivationStrictInternal( EvalState & state, - const std::string & drvName, + std::string_view drvName, const Bindings * attrs, Value & v) { @@ -1322,7 +1367,7 @@ static void derivationStrictInternal( command-line arguments to the builder. */ else if (i->name == state.sArgs) { state.forceList(*i->value, pos, context_below); - for (auto elem : i->value->listItems()) { + for (auto elem : i->value->listView()) { auto s = state.coerceToString(pos, *elem, context, "while evaluating an element of the argument list", true).toOwned(); @@ -1354,7 +1399,7 @@ static void derivationStrictInternal( /* Require ‘outputs’ to be a list of strings. */ state.forceList(*i->value, pos, context_below); Strings ss; - for (auto elem : i->value->listItems()) + for (auto elem : i->value->listView()) ss.emplace_back(state.forceStringNoCtx(*elem, pos, context_below)); handleOutputs(ss); } @@ -1383,6 +1428,8 @@ static void derivationStrictInternal( else if (i->name == state.sOutputHashMode) handleHashMode(s); else if (i->name == state.sOutputs) handleOutputs(tokenizeString(s)); + else if (i->name == state.sJson) + warn("In derivation '%s': setting structured attributes via '__json' is deprecated, and may be disallowed in future versions of Nix. Set '__structuredAttrs = true' instead.", drvName); } } @@ -1588,9 +1635,13 @@ static RegisterPrimOp primop_placeholder({ .name = "placeholder", .args = {"output"}, .doc = R"( - Return a placeholder string for the specified *output* that will be - substituted by the corresponding output path at build time. Typical - outputs would be `"out"`, `"bin"` or `"dev"`. + Return an + [output placeholder string](@docroot@/store/derivation/index.md#output-placeholder) + for the specified *output* that will be substituted by the corresponding + [output path](@docroot@/glossary.md#gloss-output-path) + at build time. + + Typical outputs would be `"out"`, `"bin"` or `"dev"`. )", .fun = prim_placeholder, }); @@ -1749,7 +1800,7 @@ static RegisterPrimOp primop_baseNameOf({ After this, the *base name* is returned as previously described, assuming `/` as the directory separator. (Note that evaluation must be platform independent.) - This is somewhat similar to the [GNU `basename`](https://www.gnu.org/software/coreutils/manual/html_node/basename-invocation.html) command, but GNU `basename` will strip any number of trailing slashes. + This is somewhat similar to the [GNU `basename`](https://www.gnu.org/software/coreutils/manual/html_node/basename-invocation.html) command, but GNU `basename` strips any number of trailing slashes. )", .fun = prim_baseNameOf, }); @@ -1831,7 +1882,7 @@ static void prim_findFile(EvalState & state, const PosIdx pos, Value * * args, V LookupPath lookupPath; - for (auto v2 : args[0]->listItems()) { + for (auto v2 : args[0]->listView()) { state.forceAttrs(*v2, pos, "while evaluating an element of the list passed to builtins.findFile"); std::string prefix; @@ -1839,7 +1890,7 @@ static void prim_findFile(EvalState & state, const PosIdx pos, Value * * args, V if (i != v2->attrs()->end()) prefix = state.forceStringNoCtx(*i->value, pos, "while evaluating the `prefix` attribute of an element of the list passed to builtins.findFile"); - i = getAttr(state, state.sPath, v2->attrs(), "in an element of the __nixPath"); + i = state.getAttr(state.sPath, v2->attrs(), "in an element of the __nixPath"); NixStringContext context; auto path = state.coerceToString(pos, *i->value, context, @@ -1848,7 +1899,7 @@ static void prim_findFile(EvalState & state, const PosIdx pos, Value * * args, V try { auto rewrites = state.realiseContext(context); - path = rewriteStrings(path, rewrites); + path = rewriteStrings(std::move(path), rewrites); } catch (InvalidPathError & e) { state.error( "cannot find '%1%', since path '%2%' is not valid", @@ -1858,8 +1909,8 @@ static void prim_findFile(EvalState & state, const PosIdx pos, Value * * args, V } lookupPath.elements.emplace_back(LookupPath::Elem { - .prefix = LookupPath::Prefix { .s = prefix }, - .path = LookupPath::Path { .s = path }, + .prefix = LookupPath::Prefix { .s = std::move(prefix) }, + .path = LookupPath::Path { .s = std::move(path) }, }); } @@ -1948,9 +1999,9 @@ static RegisterPrimOp primop_findFile(PrimOp { > ] > ``` > - > and a *lookup-path* value `"nixos-config"` will cause Nix to try `/home/eelco/Dev/nixos-config` and `/etc/nixos` in that order and return the first path that exists. + > and a *lookup-path* value `"nixos-config"` causes Nix to try `/home/eelco/Dev/nixos-config` and `/etc/nixos` in that order and return the first path that exists. - If `path` starts with `http://` or `https://`, it is interpreted as the URL of a tarball that will be downloaded and unpacked to a temporary location. + If `path` starts with `http://` or `https://`, it is interpreted as the URL of a tarball to be downloaded and unpacked to a temporary location. The tarball must consist of a single top-level directory. The URLs of the tarballs from the official `nixos.org` channels can be abbreviated as `channel:`. @@ -2047,7 +2098,7 @@ static RegisterPrimOp primop_readFileType({ .args = {"p"}, .doc = R"( Determine the directory entry type of a filesystem node, being - one of "directory", "regular", "symlink", or "unknown". + one of `"directory"`, `"regular"`, `"symlink"`, or `"unknown"`. )", .fun = prim_readFileType, }); @@ -2097,7 +2148,7 @@ static RegisterPrimOp primop_readDir({ Return the contents of the directory *path* as a set mapping directory entries to the corresponding file type. For instance, if directory `A` contains a regular file `B` and another directory - `C`, then `builtins.readDir ./A` will return the set + `C`, then `builtins.readDir ./A` returns the set ```nix { B = "regular"; C = "directory"; } @@ -2128,12 +2179,15 @@ static RegisterPrimOp primop_outputOf({ .name = "__outputOf", .args = {"derivation-reference", "output-name"}, .doc = R"( - Return the output path of a derivation, literally or using a placeholder if needed. + Return the output path of a derivation, literally or using an + [input placeholder string](@docroot@/store/derivation/index.md#input-placeholder) + if needed. - If the derivation has a statically-known output path (i.e. the derivation output is input-addressed, or fixed content-addresed), the output path will just be returned. - But if the derivation is content-addressed or if the derivation is itself not-statically produced (i.e. is the output of another derivation), a placeholder will be returned instead. + If the derivation has a statically-known output path (i.e. the derivation output is input-addressed, or fixed content-addressed), the output path is returned. + But if the derivation is content-addressed or if the derivation is itself not-statically produced (i.e. is the output of another derivation), an input placeholder is returned instead. - *`derivation reference`* must be a string that may contain a regular store path to a derivation, or may be a placeholder reference. If the derivation is produced by a derivation, you must explicitly select `drv.outPath`. + *`derivation reference`* must be a string that may contain a regular store path to a derivation, or may be an input placeholder reference. + If the derivation is produced by a derivation, you must explicitly select `drv.outPath`. This primop can be chained arbitrarily deeply. For instance, @@ -2143,9 +2197,9 @@ static RegisterPrimOp primop_outputOf({ "out" ``` - will return a placeholder for the output of the output of `myDrv`. + returns an input placeholder for the output of the output of `myDrv`. - This primop corresponds to the `^` sigil for derivable paths, e.g. as part of installable syntax on the command line. + This primop corresponds to the `^` sigil for [deriving paths](@docroot@/glossary.md#gloss-deriving-paths), e.g. as part of installable syntax on the command line. )", .fun = prim_outputOf, .experimentalFeature = Xp::DynamicDerivations, @@ -2321,8 +2375,8 @@ static RegisterPrimOp primop_fromJSON({ static void prim_toFile(EvalState & state, const PosIdx pos, Value * * args, Value & v) { NixStringContext context; - std::string name(state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.toFile")); - std::string contents(state.forceString(*args[1], context, pos, "while evaluating the second argument passed to builtins.toFile")); + auto name = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.toFile"); + auto contents = state.forceString(*args[1], context, pos, "while evaluating the second argument passed to builtins.toFile"); StorePathSet refs; @@ -2465,21 +2519,11 @@ static void addPath( const NixStringContext & context) { try { - StorePathSet refs; - if (path.accessor == state.rootFS && state.store->isInStore(path.path.abs())) { // FIXME: handle CA derivation outputs (where path needs to // be rewritten to the actual output). auto rewrites = state.realiseContext(context); - path = {state.rootFS, CanonPath(state.toRealPath(rewriteStrings(path.path.abs(), rewrites), context))}; - - try { - auto [storePath, subPath] = state.store->toStorePath(path.path.abs()); - // FIXME: we should scanForReferences on the path before adding it - refs = state.store->queryPathInfo(storePath)->references; - path = {state.rootFS, CanonPath(state.store->toRealPath(storePath) + subPath)}; - } catch (Error &) { // FIXME: should be InvalidPathError - } + path = {path.accessor, CanonPath(rewriteStrings(path.path.abs(), rewrites))}; } std::unique_ptr filter; @@ -2498,6 +2542,7 @@ static void addPath( if (!expectedHash || !state.store->isValidPath(*expectedStorePath)) { auto dstPath = fetchToStore( + state.fetchSettings, *state.store, path.resolveSymlinks(), settings.readOnlyMode ? FetchMode::DryRun : FetchMode::Copy, @@ -2538,12 +2583,12 @@ static RegisterPrimOp primop_filterSource({ > > `filterSource` should not be used to filter store paths. Since > `filterSource` uses the name of the input directory while naming - > the output directory, doing so will produce a directory name in + > the output directory, doing so produces a directory name in > the form of `--`, where `-` is > the name of the input directory. Since `` depends on the - > unfiltered directory, the name of the output directory will - > indirectly depend on files that are filtered out by the - > function. This will trigger a rebuild even when a filtered out + > unfiltered directory, the name of the output directory + > indirectly depends on files that are filtered out by the + > function. This triggers a rebuild even when a filtered out > file is changed. Use `builtins.path` instead, which allows > specifying the name of the output directory. @@ -2558,8 +2603,8 @@ static RegisterPrimOp primop_filterSource({ } ``` - However, if `source-dir` is a Subversion working copy, then all - those annoying `.svn` subdirectories will also be copied to the + However, if `source-dir` is a Subversion working copy, then all of + those annoying `.svn` subdirectories are also copied to the store. Worse, the contents of those directories may change a lot, causing lots of spurious rebuilds. With `filterSource` you can filter out the `.svn` directories: @@ -2579,8 +2624,8 @@ static RegisterPrimOp primop_filterSource({ `"regular"`, `"directory"`, `"symlink"` or `"unknown"` (for other kinds of files such as device nodes or fifos — but note that those cannot be copied to the Nix store, so if the predicate returns - `true` for them, the copy will fail). If you exclude a directory, - the entire corresponding subtree of *e2* will be excluded. + `true` for them, the copy fails). If you exclude a directory, + the entire corresponding subtree of *e2* is excluded. )", .fun = prim_filterSource, }); @@ -2588,7 +2633,7 @@ static RegisterPrimOp primop_filterSource({ static void prim_path(EvalState & state, const PosIdx pos, Value * * args, Value & v) { std::optional path; - std::string name; + std::string_view name; Value * filterFun = nullptr; auto method = ContentAddressMethod::Raw::NixArchive; std::optional expectedHash; @@ -2654,7 +2699,7 @@ static RegisterPrimOp primop_path({ - sha256\ When provided, this is the expected hash of the file at the - path. Evaluation will fail if the hash is incorrect, and + path. Evaluation fails if the hash is incorrect, and providing a hash allows `builtins.path` to be used even when the `pure-eval` nix config option is on. )", @@ -2676,7 +2721,7 @@ static void prim_attrNames(EvalState & state, const PosIdx pos, Value * * args, auto list = state.buildList(args[0]->attrs()->size()); for (const auto & [n, i] : enumerate(*args[0]->attrs())) - (list[n] = state.allocValue())->mkString(state.symbols[i.name]); + list[n] = Value::toPtr(state.symbols[i.name]); std::sort(list.begin(), list.end(), [](Value * v1, Value * v2) { return strcmp(v1->c_str(), v2->c_str()) < 0; }); @@ -2734,8 +2779,7 @@ void prim_getAttr(EvalState & state, const PosIdx pos, Value * * args, Value & v { auto attr = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.getAttr"); state.forceAttrs(*args[1], pos, "while evaluating the second argument passed to builtins.getAttr"); - auto i = getAttr( - state, + auto i = state.getAttr( state.symbols.create(attr), args[1]->attrs(), "in the attribute set under consideration" @@ -2772,11 +2816,17 @@ static void prim_unsafeGetAttrPos(EvalState & state, const PosIdx pos, Value * * static RegisterPrimOp primop_unsafeGetAttrPos(PrimOp { .name = "__unsafeGetAttrPos", + .args = {"s", "set"}, .arity = 2, + .doc = R"( + `unsafeGetAttrPos` returns the position of the attribute named *s* + from *set*. This is used by Nixpkgs to provide location information + in error messages. + )", .fun = prim_unsafeGetAttrPos, }); -// access to exact position information (ie, line and colum numbers) is deferred +// access to exact position information (ie, line and column numbers) is deferred // due to the cost associated with calculating that information and how rarely // it is used in practice. this is achieved by creating thunks to otherwise // inaccessible primops that are not exposed as __op or under builtins to turn @@ -2788,7 +2838,7 @@ static RegisterPrimOp primop_unsafeGetAttrPos(PrimOp { // but each type of thunk has an associated runtime cost in the current evaluator. // as with black holes this cost is too high to justify another thunk type to check // for in the very hot path that is forceValue. -static struct LazyPosAcessors { +static struct LazyPosAccessors { PrimOp primop_lineOfPos{ .arity = 1, .fun = [] (EvalState & state, PosIdx pos, Value * * args, Value & v) { @@ -2804,7 +2854,7 @@ static struct LazyPosAcessors { Value lineOfPos, columnOfPos; - LazyPosAcessors() + LazyPosAccessors() { lineOfPos.mkPrimOp(&primop_lineOfPos); columnOfPos.mkPrimOp(&primop_columnOfPos); @@ -2870,7 +2920,7 @@ static void prim_removeAttrs(EvalState & state, const PosIdx pos, Value * * args // 64: large enough to fit the attributes of a derivation boost::container::small_vector names; names.reserve(args[1]->listSize()); - for (auto elem : args[1]->listItems()) { + for (auto elem : args[1]->listView()) { state.forceStringNoCtx(*elem, pos, "while evaluating the values of the second argument passed to builtins.removeAttrs"); names.emplace_back(state.symbols.create(elem->string_view()), nullptr); } @@ -2912,25 +2962,48 @@ static void prim_listToAttrs(EvalState & state, const PosIdx pos, Value * * args { state.forceList(*args[0], pos, "while evaluating the argument passed to builtins.listToAttrs"); - auto attrs = state.buildBindings(args[0]->listSize()); - - std::set seen; + // Step 1. Sort the name-value attrsets in place using the memory we allocate for the result + auto listView = args[0]->listView(); + size_t listSize = listView.size(); + auto & bindings = *state.allocBindings(listSize); + using ElemPtr = decltype(&bindings[0].value); - for (auto v2 : args[0]->listItems()) { + for (const auto & [n, v2] : enumerate(listView)) { state.forceAttrs(*v2, pos, "while evaluating an element of the list passed to builtins.listToAttrs"); - auto j = getAttr(state, state.sName, v2->attrs(), "in a {name=...; value=...;} pair"); + auto j = state.getAttr(state.sName, v2->attrs(), "in a {name=...; value=...;} pair"); auto name = state.forceStringNoCtx(*j->value, j->pos, "while evaluating the `name` attribute of an element of the list passed to builtins.listToAttrs"); - auto sym = state.symbols.create(name); - if (seen.insert(sym).second) { - auto j2 = getAttr(state, state.sValue, v2->attrs(), "in a {name=...; value=...;} pair"); - attrs.insert(sym, j2->value, j2->pos); - } + + // (ab)use Attr to store a Value * * instead of a Value *, so that we can stabilize the sort using the Value * * + bindings[n] = Attr(sym, std::bit_cast(&v2)); } - v.mkAttrs(attrs); + std::sort(&bindings[0], &bindings[listSize], [](const Attr & a, const Attr & b) { + // Note that .value is actually a Value * * that corresponds to the position in the list + return a < b || (!(a > b) && std::bit_cast(a.value) < std::bit_cast(b.value)); + }); + + // Step 2. Unpack the bindings in place and skip name-value pairs with duplicate names + Symbol prev; + for (size_t n = 0; n < listSize; n++) { + auto attr = bindings[n]; + if (prev == attr.name) { + continue; + } + // Note that .value is actually a Value * *; see earlier comments + Value * v2 = *std::bit_cast(attr.value); + + auto j = state.getAttr(state.sValue, v2->attrs(), "in a {name=...; value=...;} pair"); + prev = attr.name; + bindings.push_back({prev, j->value, j->pos}); + } + // help GC and clear end of allocated array + for (size_t n = bindings.size(); n < listSize; n++) { + bindings[n] = Attr{}; + } + v.mkAttrs(&bindings); } static RegisterPrimOp primop_listToAttrs({ @@ -3050,14 +3123,14 @@ static void prim_catAttrs(EvalState & state, const PosIdx pos, Value * * args, V SmallValueVector res(args[1]->listSize()); size_t found = 0; - for (auto v2 : args[1]->listItems()) { + for (auto v2 : args[1]->listView()) { state.forceAttrs(*v2, pos, "while evaluating an element in the list passed as second argument to builtins.catAttrs"); if (auto i = v2->attrs()->get(attrName)) res[found++] = i->value; } auto list = state.buildList(found); - for (unsigned int n = 0; n < found; ++n) + for (size_t n = 0; n < found; ++n) list[n] = res[n]; v.mkList(list); } @@ -3089,15 +3162,21 @@ static void prim_functionArgs(EvalState & state, const PosIdx pos, Value * * arg if (!args[0]->isLambda()) state.error("'functionArgs' requires a function").atPos(pos).debugThrow(); - if (!args[0]->payload.lambda.fun->hasFormals()) { + if (!args[0]->lambda().fun->hasFormals()) { v.mkAttrs(&state.emptyBindings); return; } - auto attrs = state.buildBindings(args[0]->payload.lambda.fun->formals->formals.size()); - for (auto & i : args[0]->payload.lambda.fun->formals->formals) + const auto &formals = args[0]->lambda().fun->formals->formals; + auto attrs = state.buildBindings(formals.size()); + for (auto & i : formals) attrs.insert(i.name, state.getBool(i.def), i.pos); - v.mkAttrs(attrs); + /* Optimization: avoid sorting bindings. `formals` must already be sorted according to + (std::tie(a.name, a.pos) < std::tie(b.name, b.pos)) predicate, so the following assertion + always holds: + assert(std::is_sorted(attrs.alreadySorted()->begin(), attrs.alreadySorted()->end())); + .*/ + v.mkAttrs(attrs.alreadySorted()); } static RegisterPrimOp primop_functionArgs({ @@ -3125,9 +3204,8 @@ static void prim_mapAttrs(EvalState & state, const PosIdx pos, Value * * args, V auto attrs = state.buildBindings(args[1]->attrs()->size()); for (auto & i : *args[1]->attrs()) { - Value * vName = state.allocValue(); + Value * vName = Value::toPtr(state.symbols[i.name]); Value * vFun2 = state.allocValue(); - vName->mkString(state.symbols[i.name]); vFun2->mkApp(args[0], vName); attrs.alloc(i.name).mkApp(vFun2, i.value); } @@ -3170,7 +3248,7 @@ static void prim_zipAttrsWith(EvalState & state, const PosIdx pos, Value * * arg state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.zipAttrsWith"); state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.zipAttrsWith"); - const auto listItems = args[1]->listItems(); + const auto listItems = args[1]->listView(); for (auto & vElem : listItems) { state.forceAttrs(*vElem, noPos, "while evaluating a value of the list passed as second argument to builtins.zipAttrsWith"); @@ -3191,8 +3269,7 @@ static void prim_zipAttrsWith(EvalState & state, const PosIdx pos, Value * * arg auto attrs = state.buildBindings(attrsSeen.size()); for (auto & [sym, elem] : attrsSeen) { - auto name = state.allocValue(); - name->mkString(state.symbols[sym]); + auto name = Value::toPtr(state.symbols[sym]); auto call1 = state.allocValue(); call1->mkApp(args[0], name); auto call2 = state.allocValue(); @@ -3259,23 +3336,19 @@ static RegisterPrimOp primop_isList({ .fun = prim_isList, }); -static void elemAt(EvalState & state, const PosIdx pos, Value & list, int n, Value & v) -{ - state.forceList(list, pos, "while evaluating the first argument passed to builtins.elemAt"); - if (n < 0 || (unsigned int) n >= list.listSize()) - state.error( - "list index %1% is out of bounds", - n - ).atPos(pos).debugThrow(); - state.forceValue(*list.listElems()[n], pos); - v = *list.listElems()[n]; -} - /* Return the n-1'th element of a list. */ static void prim_elemAt(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - NixInt::Inner elem = state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.elemAt").value; - elemAt(state, pos, *args[0], elem, v); + NixInt::Inner n = state.forceInt(*args[1], pos, "while evaluating the second argument passed to 'builtins.elemAt'").value; + state.forceList(*args[0], pos, "while evaluating the first argument passed to 'builtins.elemAt'"); + if (n < 0 || std::make_unsigned_t(n) >= args[0]->listSize()) + state.error( + "'builtins.elemAt' called with index %d on a list of size %d", + n, + args[0]->listSize() + ).atPos(pos).debugThrow(); + state.forceValue(*args[0]->listView()[n], pos); + v = *args[0]->listView()[n]; } static RegisterPrimOp primop_elemAt({ @@ -3291,7 +3364,13 @@ static RegisterPrimOp primop_elemAt({ /* Return the first element of a list. */ static void prim_head(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - elemAt(state, pos, *args[0], 0, v); + state.forceList(*args[0], pos, "while evaluating the first argument passed to 'builtins.head'"); + if (args[0]->listSize() == 0) + state.error( + "'builtins.head' called on an empty list" + ).atPos(pos).debugThrow(); + state.forceValue(*args[0]->listView()[0], pos); + v = *args[0]->listView()[0]; } static RegisterPrimOp primop_head({ @@ -3310,13 +3389,13 @@ static RegisterPrimOp primop_head({ don't want to use it! */ static void prim_tail(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.tail"); + state.forceList(*args[0], pos, "while evaluating the first argument passed to 'builtins.tail'"); if (args[0]->listSize() == 0) - state.error("'tail' called on an empty list").atPos(pos).debugThrow(); + state.error("'builtins.tail' called on an empty list").atPos(pos).debugThrow(); auto list = state.buildList(args[0]->listSize() - 1); for (const auto & [n, v] : enumerate(list)) - v = args[0]->listElems()[n + 1]; + v = args[0]->listView()[n + 1]; v.mkList(list); } @@ -3351,7 +3430,7 @@ static void prim_map(EvalState & state, const PosIdx pos, Value * * args, Value auto list = state.buildList(args[1]->listSize()); for (const auto & [n, v] : enumerate(list)) (v = state.allocValue())->mkApp( - args[0], args[1]->listElems()[n]); + args[0], args[1]->listView()[n]); v.mkList(list); } @@ -3385,15 +3464,16 @@ static void prim_filter(EvalState & state, const PosIdx pos, Value * * args, Val state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.filter"); - SmallValueVector vs(args[1]->listSize()); + auto len = args[1]->listSize(); + SmallValueVector vs(len); size_t k = 0; bool same = true; - for (unsigned int n = 0; n < args[1]->listSize(); ++n) { + for (size_t n = 0; n < len; ++n) { Value res; - state.callFunction(*args[0], *args[1]->listElems()[n], res, noPos); + state.callFunction(*args[0], *args[1]->listView()[n], res, noPos); if (state.forceBool(res, pos, "while evaluating the return value of the filtering function passed to builtins.filter")) - vs[k++] = args[1]->listElems()[n]; + vs[k++] = args[1]->listView()[n]; else same = false; } @@ -3422,7 +3502,7 @@ static void prim_elem(EvalState & state, const PosIdx pos, Value * * args, Value { bool res = false; state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.elem"); - for (auto elem : args[1]->listItems()) + for (auto elem : args[1]->listView()) if (state.eqValues(*args[0], *elem, pos, "while searching for the presence of the given element in the list")) { res = true; break; @@ -3444,7 +3524,8 @@ static RegisterPrimOp primop_elem({ static void prim_concatLists(EvalState & state, const PosIdx pos, Value * * args, Value & v) { state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.concatLists"); - state.concatLists(v, args[0]->listSize(), args[0]->listElems(), pos, "while evaluating a value of the list passed to builtins.concatLists"); + auto listView = args[0]->listView(); + state.concatLists(v, args[0]->listSize(), listView.data(), pos, "while evaluating a value of the list passed to builtins.concatLists"); } static RegisterPrimOp primop_concatLists({ @@ -3482,7 +3563,8 @@ static void prim_foldlStrict(EvalState & state, const PosIdx pos, Value * * args if (args[2]->listSize()) { Value * vCur = args[1]; - for (auto [n, elem] : enumerate(args[2]->listItems())) { + auto listView = args[2]->listView(); + for (auto [n, elem] : enumerate(listView)) { Value * vs []{vCur, elem}; vCur = n == args[2]->listSize() - 1 ? &v : state.allocValue(); state.callFunction(*args[0], vs, *vCur, pos); @@ -3524,7 +3606,7 @@ static void anyOrAll(bool any, EvalState & state, const PosIdx pos, Value * * ar : "while evaluating the return value of the function passed to builtins.all"; Value vTmp; - for (auto elem : args[1]->listItems()) { + for (auto elem : args[1]->listView()) { state.callFunction(*args[0], *elem, vTmp, pos); bool res = state.forceBool(vTmp, pos, errorCtx); if (res == any) { @@ -3571,12 +3653,12 @@ static void prim_genList(EvalState & state, const PosIdx pos, Value * * args, Va { auto len_ = state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.genList").value; - if (len_ < 0) + if (len_ < 0 || std::make_unsigned_t(len_) > std::numeric_limits::max()) state.error("cannot create list of size %1%", len_).atPos(pos).debugThrow(); size_t len = size_t(len_); - // More strict than striclty (!) necessary, but acceptable + // More strict than strictly (!) necessary, but acceptable // as evaluating map without accessing any values makes little sense. state.forceFunction(*args[0], noPos, "while evaluating the first argument passed to builtins.genList"); @@ -3622,7 +3704,7 @@ static void prim_sort(EvalState & state, const PosIdx pos, Value * * args, Value auto list = state.buildList(len); for (const auto & [n, v] : enumerate(list)) - state.forceValue(*(v = args[1]->listElems()[n]), pos); + state.forceValue(*(v = args[1]->listView()[n]), pos); auto comparator = [&](Value * a, Value * b) { /* Optimization: if the comparator is lessThan, bypass @@ -3639,10 +3721,14 @@ static void prim_sort(EvalState & state, const PosIdx pos, Value * * args, Value return state.forceBool(vBool, pos, "while evaluating the return value of the sorting function passed to builtins.sort"); }; - /* FIXME: std::sort can segfault if the comparator is not a strict - weak ordering. What to do? std::stable_sort() seems more - resilient, but no guarantees... */ - std::stable_sort(list.begin(), list.end(), comparator); + /* NOTE: Using custom implementation because std::sort and std::stable_sort + are not resilient to comparators that violate strict weak ordering. Diagnosing + incorrect implementations is a O(n^3) problem, so doing the checks is much more + expensive that doing the sorting. For this reason we choose to use sorting algorithms + that are can't be broken by invalid comprators. peeksort (mergesort) + doesn't misbehave when any of the strict weak order properties is + violated - output is always a reordering of the input. */ + peeksort(list.begin(), list.end(), comparator); v.mkList(list); } @@ -3664,6 +3750,32 @@ static RegisterPrimOp primop_sort({ This is a stable sort: it preserves the relative order of elements deemed equal by the comparator. + + *comparator* must impose a strict weak ordering on the set of values + in the *list*. This means that for any elements *a*, *b* and *c* from the + *list*, *comparator* must satisfy the following relations: + + 1. Transitivity + + ```nix + comparator a b && comparator b c -> comparator a c + ``` + + 1. Irreflexivity + + ```nix + comparator a a == false + ``` + + 1. Transitivity of equivalence + + ```nix + let equiv = a: b: (!comparator a b && !comparator b a); in + equiv a b && equiv b c -> equiv a c + ``` + + If the *comparator* violates any of these properties, then `builtins.sort` + reorders elements in an unspecified manner. )", .fun = prim_sort, }); @@ -3677,8 +3789,8 @@ static void prim_partition(EvalState & state, const PosIdx pos, Value * * args, ValueVector right, wrong; - for (unsigned int n = 0; n < len; ++n) { - auto vElem = args[1]->listElems()[n]; + for (size_t n = 0; n < len; ++n) { + auto vElem = args[1]->listView()[n]; state.forceValue(*vElem, pos); Value res; state.callFunction(*args[0], *vElem, res, pos); @@ -3735,7 +3847,7 @@ static void prim_groupBy(EvalState & state, const PosIdx pos, Value * * args, Va ValueVectorMap attrs; - for (auto vElem : args[1]->listItems()) { + for (auto vElem : args[1]->listView()) { Value res; state.callFunction(*args[0], *vElem, res, pos); auto name = state.forceStringNoCtx(res, pos, "while evaluating the return value of the grouping function passed to builtins.groupBy"); @@ -3790,8 +3902,8 @@ static void prim_concatMap(EvalState & state, const PosIdx pos, Value * * args, SmallTemporaryValueVector lists(nrLists); size_t len = 0; - for (unsigned int n = 0; n < nrLists; ++n) { - Value * vElem = args[1]->listElems()[n]; + for (size_t n = 0; n < nrLists; ++n) { + Value * vElem = args[1]->listView()[n]; state.callFunction(*args[0], *vElem, lists[n], pos); state.forceList(lists[n], lists[n].determinePos(args[0]->determinePos(pos)), "while evaluating the return value of the function passed to builtins.concatMap"); len += lists[n].listSize(); @@ -3799,10 +3911,11 @@ static void prim_concatMap(EvalState & state, const PosIdx pos, Value * * args, auto list = state.buildList(len); auto out = list.elems; - for (unsigned int n = 0, pos = 0; n < nrLists; ++n) { - auto l = lists[n].listSize(); + for (size_t n = 0, pos = 0; n < nrLists; ++n) { + auto listView = lists[n].listView(); + auto l = listView.size(); if (l) - memcpy(out + pos, lists[n].listElems(), l * sizeof(Value *)); + memcpy(out + pos, listView.data(), l * sizeof(Value *)); pos += l; } v.mkList(list); @@ -4009,9 +4122,9 @@ static RegisterPrimOp primop_lessThan({ .name = "__lessThan", .args = {"e1", "e2"}, .doc = R"( - Return `true` if the number *e1* is less than the number *e2*, and - `false` otherwise. Evaluation aborts if either *e1* or *e2* does not - evaluate to a number. + Return `true` if the value *e1* is less than the value *e2*, and `false` otherwise. + Evaluation aborts if either *e1* or *e2* does not evaluate to a number, string or path. + Furthermore, it aborts if *e2* does not match *e1*'s type according to the aforementioned classification of number, string or path. )", .fun = prim_lessThan, }); @@ -4059,27 +4172,25 @@ static RegisterPrimOp primop_toString({ }); /* `substring start len str' returns the substring of `str' starting - at character position `min(start, stringLength str)' inclusive and + at byte position `min(start, stringLength str)' inclusive and ending at `min(start + len, stringLength str)'. `start' must be non-negative. */ static void prim_substring(EvalState & state, const PosIdx pos, Value * * args, Value & v) { + using NixUInt = std::make_unsigned_t; NixInt::Inner start = state.forceInt(*args[0], pos, "while evaluating the first argument (the start offset) passed to builtins.substring").value; if (start < 0) state.error("negative start position in 'substring'").atPos(pos).debugThrow(); - NixInt::Inner len = state.forceInt(*args[1], pos, "while evaluating the second argument (the substring length) passed to builtins.substring").value; // Negative length may be idiomatically passed to builtins.substring to get // the tail of the string. - if (len < 0) { - len = std::numeric_limits::max(); - } + auto _len = std::numeric_limits::max(); // Special-case on empty substring to avoid O(n) strlen - // This allows for the use of empty substrings to efficently capture string context + // This allows for the use of empty substrings to efficiently capture string context if (len == 0) { state.forceValue(*args[2], pos); if (args[2]->type() == nString) { @@ -4088,17 +4199,21 @@ static void prim_substring(EvalState & state, const PosIdx pos, Value * * args, } } + if (len >= 0 && NixUInt(len) < _len) { + _len = len; + } + NixStringContext context; auto s = state.coerceToString(pos, *args[2], context, "while evaluating the third argument (the string) passed to builtins.substring"); - v.mkString((unsigned int) start >= s->size() ? "" : s->substr(start, len), context); + v.mkString(NixUInt(start) >= s->size() ? "" : s->substr(start, _len), context); } static RegisterPrimOp primop_substring({ .name = "__substring", .args = {"start", "len", "s"}, .doc = R"( - Return the substring of *s* from character position *start* + Return the substring of *s* from byte position *start* (zero-based) up to but not including *start + len*. If *start* is greater than the length of the string, an empty string is returned. If *start + len* lies beyond the end of the string or *len* is `-1`, @@ -4162,7 +4277,7 @@ static void prim_convertHash(EvalState & state, const PosIdx pos, Value * * args state.forceAttrs(*args[0], pos, "while evaluating the first argument passed to builtins.convertHash"); auto inputAttrs = args[0]->attrs(); - auto iteratorHash = getAttr(state, state.symbols.create("hash"), inputAttrs, "while locating the attribute 'hash'"); + auto iteratorHash = state.getAttr(state.symbols.create("hash"), inputAttrs, "while locating the attribute 'hash'"); auto hash = state.forceStringNoCtx(*iteratorHash->value, pos, "while evaluating the attribute 'hash'"); auto iteratorHashAlgo = inputAttrs->get(state.symbols.create("hashAlgo")); @@ -4170,7 +4285,7 @@ static void prim_convertHash(EvalState & state, const PosIdx pos, Value * * args if (iteratorHashAlgo) ha = parseHashAlgo(state.forceStringNoCtx(*iteratorHashAlgo->value, pos, "while evaluating the attribute 'hashAlgo'")); - auto iteratorToHashFormat = getAttr(state, state.symbols.create("toHashFormat"), args[0]->attrs(), "while locating the attribute 'toHashFormat'"); + auto iteratorToHashFormat = state.getAttr(state.symbols.create("toHashFormat"), args[0]->attrs(), "while locating the attribute 'toHashFormat'"); HashFormat hf = parseHashFormat(state.forceStringNoCtx(*iteratorToHashFormat->value, pos, "while evaluating the attribute 'toHashFormat'")); v.mkString(Hash::parseAny(hash, ha).to_string(hf, hf == HashFormat::SRI)); @@ -4256,9 +4371,7 @@ struct RegexCache { struct State { - // TODO use C++20 transparent comparison when available - std::unordered_map cache; - std::list keys; + std::unordered_map> cache; }; Sync state_; @@ -4269,8 +4382,14 @@ struct RegexCache auto it = state->cache.find(re); if (it != state->cache.end()) return it->second; - state->keys.emplace_back(re); - return state->cache.emplace(state->keys.back(), std::regex(state->keys.back(), std::regex::extended)).first->second; + /* No std::regex constructor overload from std::string_view, but can be constructed + from a pointer + size or an iterator range. */ + return state->cache + .emplace( + std::piecewise_construct, + std::forward_as_tuple(re), + std::forward_as_tuple(/*s=*/re.data(), /*count=*/re.size(), std::regex::extended)) + .first->second; } }; @@ -4391,7 +4510,7 @@ void prim_split(EvalState & state, const PosIdx pos, Value * * args, Value & v) // Add a list for matched substrings. const size_t slen = match.size() - 1; - // Start at 1, beacause the first match is the whole string. + // Start at 1, because the first match is the whole string. auto list2 = state.buildList(slen); for (const auto & [si, v2] : enumerate(list2)) { if (!match[si + 1].matched) @@ -4472,7 +4591,7 @@ static void prim_concatStringsSep(EvalState & state, const PosIdx pos, Value * * res.reserve((args[1]->listSize() + 32) * sep.size()); bool first = true; - for (auto elem : args[1]->listItems()) { + for (auto elem : args[1]->listView()) { if (first) first = false; else res += sep; res += *state.coerceToString(pos, *elem, context, "while evaluating one element of the list of strings to concat passed to builtins.concatStringsSep"); } @@ -4500,13 +4619,13 @@ static void prim_replaceStrings(EvalState & state, const PosIdx pos, Value * * a "'from' and 'to' arguments passed to builtins.replaceStrings have different lengths" ).atPos(pos).debugThrow(); - std::vector from; + std::vector from; from.reserve(args[0]->listSize()); - for (auto elem : args[0]->listItems()) + for (auto elem : args[0]->listView()) from.emplace_back(state.forceString(*elem, pos, "while evaluating one of the strings to replace passed to builtins.replaceStrings")); - std::unordered_map cache; - auto to = args[1]->listItems(); + std::unordered_map cache; + auto to = args[1]->listView(); NixStringContext context; auto s = state.forceString(*args[2], context, pos, "while evaluating the third argument passed to builtins.replaceStrings"); @@ -4654,17 +4773,13 @@ static RegisterPrimOp primop_splitVersion({ *************************************************************/ -RegisterPrimOp::PrimOps * RegisterPrimOp::primOps; - - RegisterPrimOp::RegisterPrimOp(PrimOp && primOp) { - if (!primOps) primOps = new PrimOps; - primOps->push_back(std::move(primOp)); + primOps().push_back(std::move(primOp)); } -void EvalState::createBaseEnv() +void EvalState::createBaseEnv(const EvalSettings & evalSettings) { baseEnv.up = 0; @@ -4748,7 +4863,7 @@ void EvalState::createBaseEnv() .type = nInt, .doc = R"( Return the [Unix time](https://en.wikipedia.org/wiki/Unix_time) at first evaluation. - Repeated references to that name will re-use the initially obtained value. + Repeated references to that name re-use the initially obtained value. Example: @@ -4763,7 +4878,7 @@ void EvalState::createBaseEnv() 1683705525 ``` - The [store path](@docroot@/store/store-path.md) of a derivation depending on `currentTime` will differ for each evaluation, unless both evaluate `builtins.currentTime` in the same second. + The [store path](@docroot@/store/store-path.md) of a derivation depending on `currentTime` differs for each evaluation, unless both evaluate `builtins.currentTime` in the same second. )", .impureOnly = true, }); @@ -4914,14 +5029,18 @@ void EvalState::createBaseEnv() )", }); - if (RegisterPrimOp::primOps) - for (auto & primOp : *RegisterPrimOp::primOps) - if (experimentalFeatureSettings.isEnabled(primOp.experimentalFeature)) - { - auto primOpAdjusted = primOp; - primOpAdjusted.arity = std::max(primOp.args.size(), primOp.arity); - addPrimOp(std::move(primOpAdjusted)); - } + for (auto & primOp : RegisterPrimOp::primOps()) + if (experimentalFeatureSettings.isEnabled(primOp.experimentalFeature)) { + auto primOpAdjusted = primOp; + primOpAdjusted.arity = std::max(primOp.args.size(), primOp.arity); + addPrimOp(std::move(primOpAdjusted)); + } + + for (auto & primOp : evalSettings.extraPrimOps) { + auto primOpAdjusted = primOp; + primOpAdjusted.arity = std::max(primOp.args.size(), primOp.arity); + addPrimOp(std::move(primOpAdjusted)); + } /* Add a wrapper around the derivation primop that computes the `drvPath' and `outPath' attributes lazily. @@ -4935,7 +5054,7 @@ void EvalState::createBaseEnv() /* Now that we've added all primops, sort the `builtins' set, because attribute lookups expect it to be sorted. */ - getBuiltins().payload.attrs->sort(); + const_cast(getBuiltins().attrs())->sort(); staticBaseEnv->sort(); diff --git a/src/libexpr/primops/context.cc b/src/libexpr/primops/context.cc index ede7d97ba34..56962d6a872 100644 --- a/src/libexpr/primops/context.cc +++ b/src/libexpr/primops/context.cc @@ -1,7 +1,7 @@ -#include "primops.hh" -#include "eval-inline.hh" -#include "derivations.hh" -#include "store-api.hh" +#include "nix/expr/primops.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/store/derivations.hh" +#include "nix/store/store-api.hh" namespace nix { @@ -240,7 +240,7 @@ static RegisterPrimOp primop_getContext({ The string context tracks references to derivations within a string. It is represented as an attribute set of [store derivation](@docroot@/glossary.md#gloss-store-derivation) paths mapping to output names. - Using [string interpolation](@docroot@/language/string-interpolation.md) on a derivation will add that derivation to the string context. + Using [string interpolation](@docroot@/language/string-interpolation.md) on a derivation adds that derivation to the string context. For example, ```nix @@ -312,7 +312,7 @@ static void prim_appendContext(EvalState & state, const PosIdx pos, Value * * ar name ).atPos(i.pos).debugThrow(); } - for (auto elem : attr->value->listItems()) { + for (auto elem : attr->value->listView()) { auto outputName = state.forceStringNoCtx(*elem, attr->pos, "while evaluating an output name within a string context"); context.emplace(NixStringContextElem::Built { .drvPath = makeConstantStorePathRef(namePath), diff --git a/src/libexpr/primops/derivation.nix b/src/libexpr/primops/derivation.nix index f329ff71e32..dbb8c218688 100644 --- a/src/libexpr/primops/derivation.nix +++ b/src/libexpr/primops/derivation.nix @@ -26,27 +26,34 @@ Note that `derivation` is very bare-bones, and provides almost no commands during the build. Most likely, you'll want to use functions like `stdenv.mkDerivation` in Nixpkgs to set up a basic environment. */ -drvAttrs @ { outputs ? [ "out" ], ... }: +drvAttrs@{ + outputs ? [ "out" ], + ... +}: let strict = derivationStrict drvAttrs; - commonAttrs = drvAttrs // (builtins.listToAttrs outputsList) // - { all = map (x: x.value) outputsList; + commonAttrs = + drvAttrs + // (builtins.listToAttrs outputsList) + // { + all = map (x: x.value) outputsList; inherit drvAttrs; }; - outputToAttrListElement = outputName: - { name = outputName; - value = commonAttrs // { - outPath = builtins.getAttr outputName strict; - drvPath = strict.drvPath; - type = "derivation"; - inherit outputName; - }; + outputToAttrListElement = outputName: { + name = outputName; + value = commonAttrs // { + outPath = builtins.getAttr outputName strict; + drvPath = strict.drvPath; + type = "derivation"; + inherit outputName; }; + }; outputsList = map outputToAttrListElement outputs; -in (builtins.head outputsList).value +in +(builtins.head outputsList).value diff --git a/src/libexpr/primops/fetchClosure.cc b/src/libexpr/primops/fetchClosure.cc index 04b8d059599..4be4dac8f15 100644 --- a/src/libexpr/primops/fetchClosure.cc +++ b/src/libexpr/primops/fetchClosure.cc @@ -1,8 +1,8 @@ -#include "primops.hh" -#include "store-api.hh" -#include "realisation.hh" -#include "make-content-addressed.hh" -#include "url.hh" +#include "nix/expr/primops.hh" +#include "nix/store/store-open.hh" +#include "nix/store/realisation.hh" +#include "nix/store/make-content-addressed.hh" +#include "nix/util/url.hh" namespace nix { @@ -124,7 +124,7 @@ static void prim_fetchClosure(EvalState & state, const PosIdx pos, Value * * arg for (auto & attr : *args[0]->attrs()) { const auto & attrName = state.symbols[attr.name]; auto attrHint = [&]() -> std::string { - return "while evaluating the '" + attrName + "' attribute passed to builtins.fetchClosure"; + return fmt("while evaluating the attribute '%s' passed to builtins.fetchClosure", attrName); }; if (attrName == "fromPath") { @@ -214,7 +214,7 @@ static RegisterPrimOp primop_fetchClosure({ .doc = R"( Fetch a store path [closure](@docroot@/glossary.md#gloss-closure) from a binary cache, and return the store path as a string with context. - This function can be invoked in three ways, that we will discuss in order of preference. + This function can be invoked in three ways that we will discuss in order of preference. **Fetch a content-addressed store path** diff --git a/src/libexpr/primops/fetchMercurial.cc b/src/libexpr/primops/fetchMercurial.cc index 64e3abf2db4..189bd1f73d7 100644 --- a/src/libexpr/primops/fetchMercurial.cc +++ b/src/libexpr/primops/fetchMercurial.cc @@ -1,10 +1,10 @@ -#include "primops.hh" -#include "eval-inline.hh" -#include "eval-settings.hh" -#include "store-api.hh" -#include "fetchers.hh" -#include "url.hh" -#include "url-parts.hh" +#include "nix/expr/primops.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/expr/eval-settings.hh" +#include "nix/store/store-api.hh" +#include "nix/fetchers/fetchers.hh" +#include "nix/util/url.hh" +#include "nix/util/url-parts.hh" namespace nix { diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc index 47d1be1ccdd..5b6dd65317b 100644 --- a/src/libexpr/primops/fetchTree.cc +++ b/src/libexpr/primops/fetchTree.cc @@ -1,15 +1,15 @@ -#include "attrs.hh" -#include "primops.hh" -#include "eval-inline.hh" -#include "eval-settings.hh" -#include "store-api.hh" -#include "fetchers.hh" -#include "filetransfer.hh" -#include "registry.hh" -#include "tarball.hh" -#include "url.hh" -#include "value-to-json.hh" -#include "fetch-to-store.hh" +#include "nix/fetchers/attrs.hh" +#include "nix/expr/primops.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/expr/eval-settings.hh" +#include "nix/store/store-api.hh" +#include "nix/fetchers/fetchers.hh" +#include "nix/store/filetransfer.hh" +#include "nix/fetchers/registry.hh" +#include "nix/fetchers/tarball.hh" +#include "nix/util/url.hh" +#include "nix/expr/value-to-json.hh" +#include "nix/fetchers/fetch-to-store.hh" #include @@ -90,24 +90,26 @@ static void fetchTree( fetchers::Input input { state.fetchSettings }; NixStringContext context; std::optional type; + auto fetcher = params.isFetchGit ? "fetchGit" : "fetchTree"; if (params.isFetchGit) type = "git"; state.forceValue(*args[0], pos); if (args[0]->type() == nAttrs) { - state.forceAttrs(*args[0], pos, "while evaluating the argument passed to builtins.fetchTree"); + state.forceAttrs(*args[0], pos, fmt("while evaluating the argument passed to '%s'", fetcher)); fetchers::Attrs attrs; if (auto aType = args[0]->attrs()->get(state.sType)) { if (type) state.error( - "unexpected attribute 'type'" + "unexpected argument 'type'" ).atPos(pos).debugThrow(); - type = state.forceStringNoCtx(*aType->value, aType->pos, "while evaluating the `type` attribute passed to builtins.fetchTree"); + type = state.forceStringNoCtx(*aType->value, aType->pos, + fmt("while evaluating the `type` argument passed to '%s'", fetcher)); } else if (!type) state.error( - "attribute 'type' is missing in call to 'fetchTree'" + "argument 'type' is missing in call to '%s'", fetcher ).atPos(pos).debugThrow(); attrs.emplace("type", type.value()); @@ -127,9 +129,8 @@ static void fetchTree( else if (attr.value->type() == nInt) { auto intValue = attr.value->integer().value; - if (intValue < 0) { - state.error("negative value given for fetchTree attr %1%: %2%", state.symbols[attr.name], intValue).atPos(pos).debugThrow(); - } + if (intValue < 0) + state.error("negative value given for '%s' argument '%s': %d", fetcher, state.symbols[attr.name], intValue).atPos(pos).debugThrow(); attrs.emplace(state.symbols[attr.name], uint64_t(intValue)); } else if (state.symbols[attr.name] == "publicKeys") { @@ -137,8 +138,8 @@ static void fetchTree( attrs.emplace(state.symbols[attr.name], printValueAsJSON(state, true, *attr.value, pos, context).dump()); } else - state.error("fetchTree argument '%s' is %s while a string, Boolean or integer is expected", - state.symbols[attr.name], showType(*attr.value)).debugThrow(); + state.error("argument '%s' to '%s' is %s while a string, Boolean or integer is expected", + state.symbols[attr.name], fetcher, showType(*attr.value)).debugThrow(); } if (params.isFetchGit && !attrs.contains("exportIgnore") && (!attrs.contains("submodules") || !*fetchers::maybeGetBoolAttr(attrs, "submodules"))) { @@ -153,14 +154,14 @@ static void fetchTree( if (!params.allowNameArgument) if (auto nameIter = attrs.find("name"); nameIter != attrs.end()) state.error( - "attribute 'name' isn’t supported in call to 'fetchTree'" + "argument 'name' isn’t supported in call to '%s'", fetcher ).atPos(pos).debugThrow(); input = fetchers::Input::fromAttrs(state.fetchSettings, std::move(attrs)); } else { auto url = state.coerceToString(pos, *args[0], context, - "while evaluating the first argument passed to the fetcher", - false, false).toOwned(); + fmt("while evaluating the first argument passed to '%s'", fetcher), + false, false).toOwned(); if (params.isFetchGit) { fetchers::Attrs attrs; @@ -173,24 +174,25 @@ static void fetchTree( } else { if (!experimentalFeatureSettings.isEnabled(Xp::Flakes)) state.error( - "passing a string argument to 'fetchTree' requires the 'flakes' experimental feature" + "passing a string argument to '%s' requires the 'flakes' experimental feature", fetcher ).atPos(pos).debugThrow(); input = fetchers::Input::fromURL(state.fetchSettings, url); } } if (!state.settings.pureEval && !input.isDirect() && experimentalFeatureSettings.isEnabled(Xp::Flakes)) - input = lookupInRegistries(state.store, input).first; + input = lookupInRegistries(state.store, input, fetchers::UseRegistries::Limited).first; if (state.settings.pureEval && !input.isLocked()) { - auto fetcher = "fetchTree"; - if (params.isFetchGit) - fetcher = "fetchGit"; - - state.error( - "in pure evaluation mode, '%s' will not fetch unlocked input '%s'", - fetcher, input.to_string() - ).atPos(pos).debugThrow(); + if (input.getNarHash()) + warn( + "Input '%s' is unlocked (e.g. lacks a Git revision) but does have a NAR hash. " + "This is deprecated since such inputs are verifiable but may not be reproducible.", + input.to_string()); + else + state.error( + "in pure evaluation mode, '%s' doesn't fetch unlocked input '%s'", + fetcher, input.to_string()).atPos(pos).debugThrow(); } state.checkURI(input.toURLString()); @@ -241,7 +243,7 @@ static RegisterPrimOp primop_fetchTree({ That is, `fetchTree` is idempotent. Downloads are cached in `$XDG_CACHE_HOME/nix`. - The remote source will be fetched from the network if both are true: + The remote source is fetched from the network if both are true: - A NAR hash is supplied and the corresponding store path is not [valid](@docroot@/glossary.md#gloss-validity), that is, not available in the store > **Note** @@ -303,7 +305,7 @@ static RegisterPrimOp primop_fetchTree({ - `"tarball"` Download a tar archive and extract it into the Nix store. - This has the same underyling implementation as [`builtins.fetchTarball`](@docroot@/language/builtins.md#builtins-fetchTarball) + This has the same underlying implementation as [`builtins.fetchTarball`](@docroot@/language/builtins.md#builtins-fetchTarball) - `url` (String, required) @@ -336,7 +338,7 @@ static RegisterPrimOp primop_fetchTree({ > **Note** > - > If the URL points to a local directory, and no `ref` or `rev` is given, Nix will only consider files added to the Git index, as listed by `git ls-files` but use the *current file contents* of the Git working directory. + > If the URL points to a local directory, and no `ref` or `rev` is given, Nix only considers files added to the Git index, as listed by `git ls-files` but use the *current file contents* of the Git working directory. - `ref` (String, optional) @@ -365,6 +367,12 @@ static RegisterPrimOp primop_fetchTree({ Default: `false` + - `lfs` (Bool, optional) + + Fetch any [Git LFS](https://git-lfs.com/) files. + + Default: `false` + - `allRefs` (Bool, optional) By default, this has no effect. This becomes relevant only once `shallow` cloning is disabled. @@ -529,11 +537,12 @@ static void fetch(EvalState & state, const PosIdx pos, Value * * args, Value & v auto storePath = unpack ? fetchToStore( + state.fetchSettings, *state.store, fetchers::downloadTarball(state.store, state.fetchSettings, *url), FetchMode::Copy, name) - : fetchers::downloadFile(state.store, *url, name).storePath; + : fetchers::downloadFile(state.store, state.fetchSettings, *url, name).storePath; if (expectedHash) { auto hash = unpack @@ -672,7 +681,7 @@ static RegisterPrimOp primop_fetchGit({ This option has no effect once `shallow` cloning is enabled. By default, the `ref` value is prefixed with `refs/heads/`. - As of 2.3.0, Nix will not prefix `refs/heads/` if `ref` starts with `refs/`. + As of 2.3.0, Nix doesn't prefix `refs/heads/` if `ref` starts with `refs/`. - `submodules` (default: `false`) @@ -689,6 +698,13 @@ static RegisterPrimOp primop_fetchGit({ Make a shallow clone when fetching the Git tree. When this is enabled, the options `ref` and `allRefs` have no effect anymore. + + - `lfs` (default: `false`) + + A boolean that when `true` specifies that [Git LFS] files should be fetched. + + [Git LFS]: https://git-lfs.com/ + - `allRefs` Whether to fetch all references (eg. branches and tags) of the repository. @@ -824,7 +840,7 @@ static RegisterPrimOp primop_fetchGit({ } ``` - Nix will refetch the branch according to the [`tarball-ttl`](@docroot@/command-ref/conf-file.md#conf-tarball-ttl) setting. + Nix refetches the branch according to the [`tarball-ttl`](@docroot@/command-ref/conf-file.md#conf-tarball-ttl) setting. This behavior is disabled in [pure evaluation mode](@docroot@/command-ref/conf-file.md#conf-pure-eval). @@ -835,9 +851,9 @@ static RegisterPrimOp primop_fetchGit({ ``` If the URL points to a local directory, and no `ref` or `rev` is - given, `fetchGit` will use the current content of the checked-out - files, even if they are not committed or added to Git's index. It will - only consider files added to the Git repository, as listed by `git ls-files`. + given, `fetchGit` uses the current content of the checked-out + files, even if they are not committed or added to Git's index. It + only considers files added to the Git repository, as listed by `git ls-files`. )", .fun = prim_fetchGit, }); diff --git a/src/libexpr/primops/fromTOML.cc b/src/libexpr/primops/fromTOML.cc index 264046711a6..2a29e042420 100644 --- a/src/libexpr/primops/fromTOML.cc +++ b/src/libexpr/primops/fromTOML.cc @@ -1,5 +1,5 @@ -#include "primops.hh" -#include "eval-inline.hh" +#include "nix/expr/primops.hh" +#include "nix/expr/eval-inline.hh" #include @@ -28,8 +28,10 @@ static void prim_fromTOML(EvalState & state, const PosIdx pos, Value * * args, V auto attrs = state.buildBindings(size); - for(auto & elem : table) + for(auto & elem : table) { + forceNoNullByte(elem.first); visit(attrs.alloc(elem.first), elem.second); + } v.mkAttrs(attrs); } @@ -54,7 +56,11 @@ static void prim_fromTOML(EvalState & state, const PosIdx pos, Value * * args, V v.mkFloat(toml::get(t)); break;; case toml::value_t::string: - v.mkString(toml::get(t)); + { + auto s = toml::get(t); + forceNoNullByte(s); + v.mkString(s); + } break;; case toml::value_t::local_datetime: case toml::value_t::offset_datetime: @@ -66,7 +72,9 @@ static void prim_fromTOML(EvalState & state, const PosIdx pos, Value * * args, V attrs.alloc("_type").mkString("timestamp"); std::ostringstream s; s << t; - attrs.alloc("value").mkString(toView(s)); + auto str = toView(s); + forceNoNullByte(str); + attrs.alloc("value").mkString(str); v.mkAttrs(attrs); } else { throw std::runtime_error("Dates and times are not supported"); diff --git a/src/libexpr/primops/meson.build b/src/libexpr/primops/meson.build index f910fe23768..b8abc6409af 100644 --- a/src/libexpr/primops/meson.build +++ b/src/libexpr/primops/meson.build @@ -1,6 +1,6 @@ generated_headers += gen_header.process( 'derivation.nix', - preserve_path_from: meson.project_source_root(), + preserve_path_from : meson.project_source_root(), ) sources += files( diff --git a/src/libexpr/print-ambiguous.cc b/src/libexpr/print-ambiguous.cc index a40c98643e3..2a0b009ebfb 100644 --- a/src/libexpr/print-ambiguous.cc +++ b/src/libexpr/print-ambiguous.cc @@ -1,7 +1,7 @@ -#include "print-ambiguous.hh" -#include "print.hh" -#include "signals.hh" -#include "eval.hh" +#include "nix/expr/print-ambiguous.hh" +#include "nix/expr/print.hh" +#include "nix/util/signals.hh" +#include "nix/expr/eval.hh" namespace nix { @@ -50,11 +50,13 @@ void printAmbiguous( break; } case nList: - if (seen && v.listSize() && !seen->insert(v.listElems()).second) + /* Use pointer to the Value instead of pointer to the elements, because + that would need to explicitly handle the case of SmallList. */ + if (seen && v.listSize() && !seen->insert(&v).second) str << "«repeated»"; else { str << "[ "; - for (auto v2 : v.listItems()) { + for (auto v2 : v.listView()) { if (v2) printAmbiguous(*v2, symbols, str, seen, depth - 1); else diff --git a/src/libexpr/print.cc b/src/libexpr/print.cc index d62aaf25f78..1f0c592c157 100644 --- a/src/libexpr/print.cc +++ b/src/libexpr/print.cc @@ -2,13 +2,13 @@ #include #include -#include "print.hh" -#include "ansicolor.hh" -#include "signals.hh" -#include "store-api.hh" -#include "terminal.hh" -#include "english.hh" -#include "eval.hh" +#include "nix/expr/print.hh" +#include "nix/util/ansicolor.hh" +#include "nix/util/signals.hh" +#include "nix/store/store-api.hh" +#include "nix/util/terminal.hh" +#include "nix/util/english.hh" +#include "nix/expr/eval.hh" namespace nix { @@ -415,8 +415,8 @@ class Printer if (depth < options.maxDepth) { increaseIndent(); output << "["; - auto listItems = v.listItems(); - auto prettyPrint = shouldPrettyPrintList(listItems); + auto listItems = v.listView(); + auto prettyPrint = shouldPrettyPrintList(listItems.span()); size_t currentListItemsPrinted = 0; @@ -453,13 +453,13 @@ class Printer if (v.isLambda()) { output << "lambda"; - if (v.payload.lambda.fun) { - if (v.payload.lambda.fun->name) { - output << " " << state.symbols[v.payload.lambda.fun->name]; + if (v.lambda().fun) { + if (v.lambda().fun->name) { + output << " " << state.symbols[v.lambda().fun->name]; } std::ostringstream s; - s << state.positions[v.payload.lambda.fun->pos]; + s << state.positions[v.lambda().fun->pos]; output << " @ " << filterANSIEscapes(toView(s)); } } else if (v.isPrimOp()) { diff --git a/src/libexpr/search-path.cc b/src/libexpr/search-path.cc index 657744e745c..76aecd4e5eb 100644 --- a/src/libexpr/search-path.cc +++ b/src/libexpr/search-path.cc @@ -1,4 +1,4 @@ -#include "search-path.hh" +#include "nix/expr/search-path.hh" namespace nix { diff --git a/src/libexpr/symbol-table.hh b/src/libexpr/symbol-table.hh deleted file mode 100644 index be12f6248dc..00000000000 --- a/src/libexpr/symbol-table.hh +++ /dev/null @@ -1,146 +0,0 @@ -#pragma once -///@file - -#include -#include -#include - -#include "types.hh" -#include "chunked-vector.hh" -#include "error.hh" - -namespace nix { - -/** - * This class mainly exists to give us an operator<< for ostreams. We could also - * return plain strings from SymbolTable, but then we'd have to wrap every - * instance of a symbol that is fmt()ed, which is inconvenient and error-prone. - */ -class SymbolStr -{ - friend class SymbolTable; - -private: - const std::string * s; - - explicit SymbolStr(const std::string & symbol): s(&symbol) {} - -public: - bool operator == (std::string_view s2) const - { - return *s == s2; - } - - const char * c_str() const - { - return s->c_str(); - } - - operator const std::string_view () const - { - return *s; - } - - friend std::ostream & operator <<(std::ostream & os, const SymbolStr & symbol); - - bool empty() const - { - return s->empty(); - } -}; - -/** - * Symbols have the property that they can be compared efficiently - * (using an equality test), because the symbol table stores only one - * copy of each string. - */ -class Symbol -{ - friend class SymbolTable; - -private: - uint32_t id; - - explicit Symbol(uint32_t id): id(id) {} - -public: - Symbol() : id(0) {} - - explicit operator bool() const { return id > 0; } - - auto operator<=>(const Symbol other) const { return id <=> other.id; } - bool operator==(const Symbol other) const { return id == other.id; } - - friend class std::hash; -}; - -/** - * Symbol table used by the parser and evaluator to represent and look - * up identifiers and attributes efficiently. - */ -class SymbolTable -{ -private: - std::unordered_map> symbols; - ChunkedVector store{16}; - -public: - - /** - * converts a string into a symbol. - */ - Symbol create(std::string_view s) - { - // Most symbols are looked up more than once, so we trade off insertion performance - // for lookup performance. - // TODO: could probably be done more efficiently with transparent Hash and Equals - // on the original implementation using unordered_set - // FIXME: make this thread-safe. - auto it = symbols.find(s); - if (it != symbols.end()) return Symbol(it->second.second + 1); - - const auto & [rawSym, idx] = store.add(std::string(s)); - symbols.emplace(rawSym, std::make_pair(&rawSym, idx)); - return Symbol(idx + 1); - } - - std::vector resolve(const std::vector & symbols) const - { - std::vector result; - result.reserve(symbols.size()); - for (auto sym : symbols) - result.push_back((*this)[sym]); - return result; - } - - SymbolStr operator[](Symbol s) const - { - if (s.id == 0 || s.id > store.size()) - unreachable(); - return SymbolStr(store[s.id - 1]); - } - - size_t size() const - { - return store.size(); - } - - size_t totalSize() const; - - template - void dump(T callback) const - { - store.forEach(callback); - } -}; - -} - -template<> -struct std::hash -{ - std::size_t operator()(const nix::Symbol & s) const noexcept - { - return std::hash{}(s.id); - } -}; diff --git a/src/libexpr/value-to-json.cc b/src/libexpr/value-to-json.cc index 8044fe3472e..a9b51afa074 100644 --- a/src/libexpr/value-to-json.cc +++ b/src/libexpr/value-to-json.cc @@ -1,7 +1,7 @@ -#include "value-to-json.hh" -#include "eval-inline.hh" -#include "store-api.hh" -#include "signals.hh" +#include "nix/expr/value-to-json.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/store/store-api.hh" +#include "nix/util/signals.hh" #include #include @@ -10,6 +10,7 @@ namespace nix { using json = nlohmann::json; +// TODO: rename. It doesn't print. json printValueAsJSON(EvalState & state, bool strict, Value & v, const PosIdx pos, NixStringContext & context, bool copyToStore) { @@ -72,7 +73,7 @@ json printValueAsJSON(EvalState & state, bool strict, case nList: { out = json::array(); int i = 0; - for (auto elem : v.listItems()) { + for (auto elem : v.listView()) { try { out.push_back(printValueAsJSON(state, strict, *elem, pos, context, copyToStore)); } catch (Error & e) { @@ -108,7 +109,11 @@ json printValueAsJSON(EvalState & state, bool strict, void printValueAsJSON(EvalState & state, bool strict, Value & v, const PosIdx pos, std::ostream & str, NixStringContext & context, bool copyToStore) { - str << printValueAsJSON(state, strict, v, pos, context, copyToStore); + try { + str << printValueAsJSON(state, strict, v, pos, context, copyToStore); + } catch (nlohmann::json::exception & e) { + throw JSONSerializationError("JSON serialization error: %s", e.what()); + } } json ExternalValueBase::printValueAsJSON(EvalState & state, bool strict, diff --git a/src/libexpr/value-to-xml.cc b/src/libexpr/value-to-xml.cc index 9734ebec498..235ef262760 100644 --- a/src/libexpr/value-to-xml.cc +++ b/src/libexpr/value-to-xml.cc @@ -1,7 +1,7 @@ -#include "value-to-xml.hh" -#include "xml-writer.hh" -#include "eval-inline.hh" -#include "signals.hh" +#include "nix/expr/value-to-xml.hh" +#include "nix/util/xml-writer.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/util/signals.hh" #include @@ -114,7 +114,7 @@ static void printValueAsXML(EvalState & state, bool strict, bool location, case nList: { XMLOpenElement _(doc, "list"); - for (auto v2 : v.listItems()) + for (auto v2 : v.listView()) printValueAsXML(state, strict, location, *v2, doc, context, drvsSeen, pos); break; } @@ -126,18 +126,18 @@ static void printValueAsXML(EvalState & state, bool strict, bool location, break; } XMLAttrs xmlAttrs; - if (location) posToXML(state, xmlAttrs, state.positions[v.payload.lambda.fun->pos]); + if (location) posToXML(state, xmlAttrs, state.positions[v.lambda().fun->pos]); XMLOpenElement _(doc, "function", xmlAttrs); - if (v.payload.lambda.fun->hasFormals()) { + if (v.lambda().fun->hasFormals()) { XMLAttrs attrs; - if (v.payload.lambda.fun->arg) attrs["name"] = state.symbols[v.payload.lambda.fun->arg]; - if (v.payload.lambda.fun->formals->ellipsis) attrs["ellipsis"] = "1"; + if (v.lambda().fun->arg) attrs["name"] = state.symbols[v.lambda().fun->arg]; + if (v.lambda().fun->formals->ellipsis) attrs["ellipsis"] = "1"; XMLOpenElement _(doc, "attrspat", attrs); - for (auto & i : v.payload.lambda.fun->formals->lexicographicOrder(state.symbols)) + for (auto & i : v.lambda().fun->formals->lexicographicOrder(state.symbols)) doc.writeEmptyElement("attr", singletonAttrs("name", state.symbols[i.name])); } else - doc.writeEmptyElement("varpat", singletonAttrs("name", state.symbols[v.payload.lambda.fun->arg])); + doc.writeEmptyElement("varpat", singletonAttrs("name", state.symbols[v.lambda().fun->arg])); break; } diff --git a/src/libexpr/value.hh b/src/libexpr/value.hh deleted file mode 100644 index d9816148852..00000000000 --- a/src/libexpr/value.hh +++ /dev/null @@ -1,513 +0,0 @@ -#pragma once -///@file - -#include -#include - -#include "eval-gc.hh" -#include "symbol-table.hh" -#include "value/context.hh" -#include "source-path.hh" -#include "print-options.hh" -#include "checked-arithmetic.hh" - -#include - -namespace nix { - -struct Value; -class BindingsBuilder; - - -typedef enum { - tUninitialized = 0, - tInt = 1, - tBool, - tString, - tPath, - tNull, - tAttrs, - tList1, - tList2, - tListN, - tThunk, - tApp, - tLambda, - tPrimOp, - tPrimOpApp, - tExternal, - tFloat -} InternalType; - -/** - * This type abstracts over all actual value types in the language, - * grouping together implementation details like tList*, different function - * types, and types in non-normal form (so thunks and co.) - */ -typedef enum { - nThunk, - nInt, - nFloat, - nBool, - nString, - nPath, - nNull, - nAttrs, - nList, - nFunction, - nExternal -} ValueType; - -class Bindings; -struct Env; -struct Expr; -struct ExprLambda; -struct ExprBlackHole; -struct PrimOp; -class Symbol; -class PosIdx; -struct Pos; -class StorePath; -class EvalState; -class XMLWriter; -class Printer; - -using NixInt = checked::Checked; -using NixFloat = double; - -/** - * External values must descend from ExternalValueBase, so that - * type-agnostic nix functions (e.g. showType) can be implemented - */ -class ExternalValueBase -{ - friend std::ostream & operator << (std::ostream & str, const ExternalValueBase & v); - friend class Printer; - protected: - /** - * Print out the value - */ - virtual std::ostream & print(std::ostream & str) const = 0; - - public: - /** - * Return a simple string describing the type - */ - virtual std::string showType() const = 0; - - /** - * Return a string to be used in builtins.typeOf - */ - virtual std::string typeOf() const = 0; - - /** - * Coerce the value to a string. Defaults to uncoercable, i.e. throws an - * error. - */ - virtual std::string coerceToString(EvalState & state, const PosIdx & pos, NixStringContext & context, bool copyMore, bool copyToStore) const; - - /** - * Compare to another value of the same type. Defaults to uncomparable, - * i.e. always false. - */ - virtual bool operator ==(const ExternalValueBase & b) const noexcept; - - /** - * Print the value as JSON. Defaults to unconvertable, i.e. throws an error - */ - virtual nlohmann::json printValueAsJSON(EvalState & state, bool strict, - NixStringContext & context, bool copyToStore = true) const; - - /** - * Print the value as XML. Defaults to unevaluated - */ - virtual void printValueAsXML(EvalState & state, bool strict, bool location, - XMLWriter & doc, NixStringContext & context, PathSet & drvsSeen, - const PosIdx pos) const; - - virtual ~ExternalValueBase() - { - }; -}; - -std::ostream & operator << (std::ostream & str, const ExternalValueBase & v); - - -class ListBuilder -{ - const size_t size; - Value * inlineElems[2] = {nullptr, nullptr}; -public: - Value * * elems; - ListBuilder(EvalState & state, size_t size); - - // NOTE: Can be noexcept because we are just copying integral values and - // raw pointers. - ListBuilder(ListBuilder && x) noexcept - : size(x.size) - , inlineElems{x.inlineElems[0], x.inlineElems[1]} - , elems(size <= 2 ? inlineElems : x.elems) - { } - - Value * & operator [](size_t n) - { - return elems[n]; - } - - typedef Value * * iterator; - - iterator begin() { return &elems[0]; } - iterator end() { return &elems[size]; } - - friend struct Value; -}; - - -struct Value -{ -private: - InternalType internalType = tUninitialized; - - friend std::string showType(const Value & v); - -public: - - void print(EvalState &state, std::ostream &str, PrintOptions options = PrintOptions {}); - - // Functions needed to distinguish the type - // These should be removed eventually, by putting the functionality that's - // needed by callers into methods of this type - - // type() == nThunk - inline bool isThunk() const { return internalType == tThunk; }; - inline bool isApp() const { return internalType == tApp; }; - inline bool isBlackhole() const; - - // type() == nFunction - inline bool isLambda() const { return internalType == tLambda; }; - inline bool isPrimOp() const { return internalType == tPrimOp; }; - inline bool isPrimOpApp() const { return internalType == tPrimOpApp; }; - - /** - * Strings in the evaluator carry a so-called `context` which - * is a list of strings representing store paths. This is to - * allow users to write things like - * - * "--with-freetype2-library=" + freetype + "/lib" - * - * where `freetype` is a derivation (or a source to be copied - * to the store). If we just concatenated the strings without - * keeping track of the referenced store paths, then if the - * string is used as a derivation attribute, the derivation - * will not have the correct dependencies in its inputDrvs and - * inputSrcs. - - * The semantics of the context is as follows: when a string - * with context C is used as a derivation attribute, then the - * derivations in C will be added to the inputDrvs of the - * derivation, and the other store paths in C will be added to - * the inputSrcs of the derivations. - - * For canonicity, the store paths should be in sorted order. - */ - struct StringWithContext { - const char * c_str; - const char * * context; // must be in sorted order - }; - - struct Path { - SourceAccessor * accessor; - const char * path; - }; - - struct ClosureThunk { - Env * env; - Expr * expr; - }; - - struct FunctionApplicationThunk { - Value * left, * right; - }; - - struct Lambda { - Env * env; - ExprLambda * fun; - }; - - using Payload = union - { - NixInt integer; - bool boolean; - - StringWithContext string; - - Path path; - - Bindings * attrs; - struct { - size_t size; - Value * const * elems; - } bigList; - Value * smallList[2]; - ClosureThunk thunk; - FunctionApplicationThunk app; - Lambda lambda; - PrimOp * primOp; - FunctionApplicationThunk primOpApp; - ExternalValueBase * external; - NixFloat fpoint; - }; - - Payload payload; - - /** - * Returns the normal type of a Value. This only returns nThunk if - * the Value hasn't been forceValue'd - * - * @param invalidIsThunk Instead of aborting an an invalid (probably - * 0, so uninitialized) internal type, return `nThunk`. - */ - inline ValueType type(bool invalidIsThunk = false) const - { - switch (internalType) { - case tUninitialized: break; - case tInt: return nInt; - case tBool: return nBool; - case tString: return nString; - case tPath: return nPath; - case tNull: return nNull; - case tAttrs: return nAttrs; - case tList1: case tList2: case tListN: return nList; - case tLambda: case tPrimOp: case tPrimOpApp: return nFunction; - case tExternal: return nExternal; - case tFloat: return nFloat; - case tThunk: case tApp: return nThunk; - } - if (invalidIsThunk) - return nThunk; - else - unreachable(); - } - - inline void finishValue(InternalType newType, Payload newPayload) - { - payload = newPayload; - internalType = newType; - } - - /** - * A value becomes valid when it is initialized. We don't use this - * in the evaluator; only in the bindings, where the slight extra - * cost is warranted because of inexperienced callers. - */ - inline bool isValid() const - { - return internalType != tUninitialized; - } - - inline void mkInt(NixInt::Inner n) - { - mkInt(NixInt{n}); - } - - inline void mkInt(NixInt n) - { - finishValue(tInt, { .integer = n }); - } - - inline void mkBool(bool b) - { - finishValue(tBool, { .boolean = b }); - } - - inline void mkString(const char * s, const char * * context = 0) - { - finishValue(tString, { .string = { .c_str = s, .context = context } }); - } - - void mkString(std::string_view s); - - void mkString(std::string_view s, const NixStringContext & context); - - void mkStringMove(const char * s, const NixStringContext & context); - - inline void mkString(const SymbolStr & s) - { - mkString(s.c_str()); - } - - void mkPath(const SourcePath & path); - void mkPath(std::string_view path); - - inline void mkPath(SourceAccessor * accessor, const char * path) - { - finishValue(tPath, { .path = { .accessor = accessor, .path = path } }); - } - - inline void mkNull() - { - finishValue(tNull, {}); - } - - inline void mkAttrs(Bindings * a) - { - finishValue(tAttrs, { .attrs = a }); - } - - Value & mkAttrs(BindingsBuilder & bindings); - - void mkList(const ListBuilder & builder) - { - if (builder.size == 1) - finishValue(tList1, { .smallList = { builder.inlineElems[0] } }); - else if (builder.size == 2) - finishValue(tList2, { .smallList = { builder.inlineElems[0], builder.inlineElems[1] } }); - else - finishValue(tListN, { .bigList = { .size = builder.size, .elems = builder.elems } }); - } - - inline void mkThunk(Env * e, Expr * ex) - { - finishValue(tThunk, { .thunk = { .env = e, .expr = ex } }); - } - - inline void mkApp(Value * l, Value * r) - { - finishValue(tApp, { .app = { .left = l, .right = r } }); - } - - inline void mkLambda(Env * e, ExprLambda * f) - { - finishValue(tLambda, { .lambda = { .env = e, .fun = f } }); - } - - inline void mkBlackhole(); - - void mkPrimOp(PrimOp * p); - - inline void mkPrimOpApp(Value * l, Value * r) - { - finishValue(tPrimOpApp, { .primOpApp = { .left = l, .right = r } }); - } - - /** - * For a `tPrimOpApp` value, get the original `PrimOp` value. - */ - const PrimOp * primOpAppPrimOp() const; - - inline void mkExternal(ExternalValueBase * e) - { - finishValue(tExternal, { .external = e }); - } - - inline void mkFloat(NixFloat n) - { - finishValue(tFloat, { .fpoint = n }); - } - - bool isList() const - { - return internalType == tList1 || internalType == tList2 || internalType == tListN; - } - - Value * const * listElems() - { - return internalType == tList1 || internalType == tList2 ? payload.smallList : payload.bigList.elems; - } - - std::span listItems() const - { - assert(isList()); - return std::span(listElems(), listSize()); - } - - Value * const * listElems() const - { - return internalType == tList1 || internalType == tList2 ? payload.smallList : payload.bigList.elems; - } - - size_t listSize() const - { - return internalType == tList1 ? 1 : internalType == tList2 ? 2 : payload.bigList.size; - } - - PosIdx determinePos(const PosIdx pos) const; - - /** - * Check whether forcing this value requires a trivial amount of - * computation. In particular, function applications are - * non-trivial. - */ - bool isTrivial() const; - - SourcePath path() const - { - assert(internalType == tPath); - return SourcePath( - ref(payload.path.accessor->shared_from_this()), - CanonPath(CanonPath::unchecked_t(), payload.path.path)); - } - - std::string_view string_view() const - { - assert(internalType == tString); - return std::string_view(payload.string.c_str); - } - - const char * c_str() const - { - assert(internalType == tString); - return payload.string.c_str; - } - - const char * * context() const - { - return payload.string.context; - } - - ExternalValueBase * external() const - { return payload.external; } - - const Bindings * attrs() const - { return payload.attrs; } - - const PrimOp * primOp() const - { return payload.primOp; } - - bool boolean() const - { return payload.boolean; } - - NixInt integer() const - { return payload.integer; } - - NixFloat fpoint() const - { return payload.fpoint; } -}; - - -extern ExprBlackHole eBlackHole; - -bool Value::isBlackhole() const -{ - return internalType == tThunk && payload.thunk.expr == (Expr*) &eBlackHole; -} - -void Value::mkBlackhole() -{ - mkThunk(nullptr, (Expr *) &eBlackHole); -} - - -typedef std::vector> ValueVector; -typedef std::unordered_map, std::equal_to, traceable_allocator>> ValueMap; -typedef std::map, traceable_allocator>> ValueVectorMap; - - -/** - * A value allocated in traceable memory. - */ -typedef std::shared_ptr RootValue; - -RootValue allocRootValue(Value * v); - -} diff --git a/src/libexpr/value/context.cc b/src/libexpr/value/context.cc index 6d9633268df..40d08da59ec 100644 --- a/src/libexpr/value/context.cc +++ b/src/libexpr/value/context.cc @@ -1,5 +1,5 @@ -#include "util.hh" -#include "value/context.hh" +#include "nix/util/util.hh" +#include "nix/expr/value/context.hh" #include diff --git a/src/libfetchers-c/.version b/src/libfetchers-c/.version new file mode 120000 index 00000000000..b7badcd0cc8 --- /dev/null +++ b/src/libfetchers-c/.version @@ -0,0 +1 @@ +../../.version \ No newline at end of file diff --git a/src/libfetchers-c/meson.build b/src/libfetchers-c/meson.build new file mode 100644 index 00000000000..e34997f0984 --- /dev/null +++ b/src/libfetchers-c/meson.build @@ -0,0 +1,65 @@ +project('nix-fetchers-c', 'cpp', + version : files('.version'), + default_options : [ + 'cpp_std=c++2a', + # TODO(Qyriad): increase the warning level + 'warning_level=1', + 'errorlogs=true', # Please print logs for tests that fail + ], + meson_version : '>= 1.1', + license : 'LGPL-2.1-or-later', +) + +cxx = meson.get_compiler('cpp') + +subdir('nix-meson-build-support/deps-lists') + +deps_private_maybe_subproject = [ + dependency('nix-util'), + dependency('nix-store'), + dependency('nix-fetchers'), +] +deps_public_maybe_subproject = [ + dependency('nix-util-c'), + dependency('nix-store-c'), +] +subdir('nix-meson-build-support/subprojects') + +add_project_arguments( + language : 'cpp', +) + +subdir('nix-meson-build-support/common') + +sources = files( + 'nix_api_fetchers.cc', +) + +include_dirs = [include_directories('.')] + +headers = files( + 'nix_api_fetchers.h', + 'nix_api_fetchers_internal.hh', +) + +# TODO move this header to libexpr, maybe don't use it in tests? +headers += files('nix_api_fetchers.h') + +subdir('nix-meson-build-support/export-all-symbols') +subdir('nix-meson-build-support/windows-version') + +this_library = library( + 'nixfetchersc', + sources, + dependencies : deps_public + deps_private + deps_other, + include_directories : include_dirs, + link_args: linker_export_flags, + prelink : true, # For C++ static initializers + install : true, +) + +install_headers(headers, preserve_path : true) + +libraries_private = [] + +subdir('nix-meson-build-support/export') diff --git a/src/libfetchers-c/nix-meson-build-support b/src/libfetchers-c/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/libfetchers-c/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/libfetchers-c/nix_api_fetchers.cc b/src/libfetchers-c/nix_api_fetchers.cc new file mode 100644 index 00000000000..4e8037a5e5d --- /dev/null +++ b/src/libfetchers-c/nix_api_fetchers.cc @@ -0,0 +1,19 @@ +#include "nix_api_fetchers.h" +#include "nix_api_fetchers_internal.hh" +#include "nix_api_util_internal.h" + +nix_fetchers_settings * nix_fetchers_settings_new(nix_c_context * context) +{ + try { + auto fetchersSettings = nix::make_ref(nix::fetchers::Settings{}); + return new nix_fetchers_settings{ + .settings = fetchersSettings, + }; + } + NIXC_CATCH_ERRS_NULL +} + +void nix_fetchers_settings_free(nix_fetchers_settings * settings) +{ + delete settings; +} diff --git a/src/libfetchers-c/nix_api_fetchers.h b/src/libfetchers-c/nix_api_fetchers.h new file mode 100644 index 00000000000..19da112a623 --- /dev/null +++ b/src/libfetchers-c/nix_api_fetchers.h @@ -0,0 +1,32 @@ +#ifndef NIX_API_FETCHERS_H +#define NIX_API_FETCHERS_H +/** @defgroup libfetchers libfetchers + * @brief Bindings to the Nix fetchers library + * @{ + */ +/** @file + * @brief Main entry for the libfetchers C bindings + */ + +#include "nix_api_util.h" + +#ifdef __cplusplus +extern "C" { +#endif +// cffi start + +// Type definitions +/** + * @brief Shared settings object + */ +typedef struct nix_fetchers_settings nix_fetchers_settings; + +nix_fetchers_settings * nix_fetchers_settings_new(nix_c_context * context); + +void nix_fetchers_settings_free(nix_fetchers_settings * settings); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // NIX_API_FETCHERS_H \ No newline at end of file diff --git a/src/libfetchers-c/nix_api_fetchers_internal.hh b/src/libfetchers-c/nix_api_fetchers_internal.hh new file mode 100644 index 00000000000..b0dea575460 --- /dev/null +++ b/src/libfetchers-c/nix_api_fetchers_internal.hh @@ -0,0 +1,12 @@ +#pragma once +#include "nix/fetchers/fetch-settings.hh" +#include "nix/util/ref.hh" + +/** + * A shared reference to `nix::fetchers::Settings` + * @see nix::fetchers::Settings + */ +struct nix_fetchers_settings +{ + nix::ref settings; +}; diff --git a/src/libfetchers-c/package.nix b/src/libfetchers-c/package.nix new file mode 100644 index 00000000000..9a601d70417 --- /dev/null +++ b/src/libfetchers-c/package.nix @@ -0,0 +1,50 @@ +{ + lib, + mkMesonLibrary, + + nix-store-c, + nix-expr-c, + nix-util-c, + nix-fetchers, + + # Configuration Options + + version, +}: + +let + inherit (lib) fileset; +in + +mkMesonLibrary (finalAttrs: { + pname = "nix-fetchers-c"; + inherit version; + + workDir = ./.; + fileset = fileset.unions [ + ../../nix-meson-build-support + ./nix-meson-build-support + ../../.version + ./.version + ./meson.build + # ./meson.options + (fileset.fileFilter (file: file.hasExt "cc") ./.) + (fileset.fileFilter (file: file.hasExt "hh") ./.) + (fileset.fileFilter (file: file.hasExt "h") ./.) + ]; + + propagatedBuildInputs = [ + nix-util-c + nix-expr-c + nix-store-c + nix-fetchers + ]; + + mesonFlags = [ + ]; + + meta = { + platforms = lib.platforms.unix ++ lib.platforms.windows; + }; + +}) diff --git a/src/libfetchers-tests/access-tokens.cc b/src/libfetchers-tests/access-tokens.cc new file mode 100644 index 00000000000..93043ba3efd --- /dev/null +++ b/src/libfetchers-tests/access-tokens.cc @@ -0,0 +1,101 @@ +#include +#include + +#include "nix/fetchers/fetchers.hh" +#include "nix/fetchers/fetch-settings.hh" +#include "nix/util/json-utils.hh" +#include "nix/util/tests/characterization.hh" + +namespace nix::fetchers { + +using nlohmann::json; + +class AccessKeysTest : public ::testing::Test +{ +protected: + +public: + void SetUp() override + { + experimentalFeatureSettings.experimentalFeatures.get().insert(Xp::Flakes); + } + void TearDown() override {} +}; + +TEST_F(AccessKeysTest, singleOrgGitHub) +{ + fetchers::Settings fetchSettings = fetchers::Settings{}; + fetchSettings.accessTokens.get().insert({"github.com/a", "token"}); + auto i = Input::fromURL(fetchSettings, "github:a/b"); + + auto token = i.scheme->getAccessToken(fetchSettings, "github.com", "github.com/a/b"); + ASSERT_EQ(token, "token"); +} + +TEST_F(AccessKeysTest, nonMatches) +{ + fetchers::Settings fetchSettings = fetchers::Settings{}; + fetchSettings.accessTokens.get().insert({"github.com", "token"}); + auto i = Input::fromURL(fetchSettings, "gitlab:github.com/evil"); + + auto token = i.scheme->getAccessToken(fetchSettings, "gitlab.com", "gitlab.com/github.com/evil"); + ASSERT_EQ(token, std::nullopt); +} + +TEST_F(AccessKeysTest, noPartialMatches) +{ + fetchers::Settings fetchSettings = fetchers::Settings{}; + fetchSettings.accessTokens.get().insert({"github.com/partial", "token"}); + auto i = Input::fromURL(fetchSettings, "github:partial-match/repo"); + + auto token = i.scheme->getAccessToken(fetchSettings, "github.com", "github.com/partial-match"); + ASSERT_EQ(token, std::nullopt); +} + +TEST_F(AccessKeysTest, repoGitHub) +{ + fetchers::Settings fetchSettings = fetchers::Settings{}; + fetchSettings.accessTokens.get().insert({"github.com", "token"}); + fetchSettings.accessTokens.get().insert({"github.com/a/b", "another_token"}); + fetchSettings.accessTokens.get().insert({"github.com/a/c", "yet_another_token"}); + auto i = Input::fromURL(fetchSettings, "github:a/a"); + + auto token = i.scheme->getAccessToken(fetchSettings, "github.com", "github.com/a/a"); + ASSERT_EQ(token, "token"); + + token = i.scheme->getAccessToken(fetchSettings, "github.com", "github.com/a/b"); + ASSERT_EQ(token, "another_token"); + + token = i.scheme->getAccessToken(fetchSettings, "github.com", "github.com/a/c"); + ASSERT_EQ(token, "yet_another_token"); +} + +TEST_F(AccessKeysTest, multipleGitLab) +{ + fetchers::Settings fetchSettings = fetchers::Settings{}; + fetchSettings.accessTokens.get().insert({"gitlab.com", "token"}); + fetchSettings.accessTokens.get().insert({"gitlab.com/a/b", "another_token"}); + auto i = Input::fromURL(fetchSettings, "gitlab:a/b"); + + auto token = i.scheme->getAccessToken(fetchSettings, "gitlab.com", "gitlab.com/a/b"); + ASSERT_EQ(token, "another_token"); + + token = i.scheme->getAccessToken(fetchSettings, "gitlab.com", "gitlab.com/a/c"); + ASSERT_EQ(token, "token"); +} + +TEST_F(AccessKeysTest, multipleSourceHut) +{ + fetchers::Settings fetchSettings = fetchers::Settings{}; + fetchSettings.accessTokens.get().insert({"git.sr.ht", "token"}); + fetchSettings.accessTokens.get().insert({"git.sr.ht/~a/b", "another_token"}); + auto i = Input::fromURL(fetchSettings, "sourcehut:a/b"); + + auto token = i.scheme->getAccessToken(fetchSettings, "git.sr.ht", "git.sr.ht/~a/b"); + ASSERT_EQ(token, "another_token"); + + token = i.scheme->getAccessToken(fetchSettings, "git.sr.ht", "git.sr.ht/~a/c"); + ASSERT_EQ(token, "token"); +} + +} diff --git a/src/libfetchers-tests/build-utils-meson b/src/libfetchers-tests/build-utils-meson deleted file mode 120000 index 5fff21bab55..00000000000 --- a/src/libfetchers-tests/build-utils-meson +++ /dev/null @@ -1 +0,0 @@ -../../build-utils-meson \ No newline at end of file diff --git a/src/libfetchers-tests/git-utils.cc b/src/libfetchers-tests/git-utils.cc index 0bf3076dca8..c2c7f9da08d 100644 --- a/src/libfetchers-tests/git-utils.cc +++ b/src/libfetchers-tests/git-utils.cc @@ -1,19 +1,20 @@ -#include "git-utils.hh" -#include "file-system.hh" -#include "gmock/gmock.h" +#include "nix/fetchers/git-utils.hh" +#include "nix/util/file-system.hh" +#include #include #include #include #include -#include "fs-sink.hh" -#include "serialise.hh" +#include "nix/util/fs-sink.hh" +#include "nix/util/serialise.hh" +#include "nix/fetchers/git-lfs-fetch.hh" namespace nix { class GitUtilsTest : public ::testing::Test { // We use a single repository for all tests. - Path tmpDir; + std::filesystem::path tmpDir; std::unique_ptr delTmpDir; public: @@ -25,7 +26,7 @@ class GitUtilsTest : public ::testing::Test // Create the repo with libgit2 git_libgit2_init(); git_repository * repo = nullptr; - auto r = git_repository_init(&repo, tmpDir.c_str(), 0); + auto r = git_repository_init(&repo, tmpDir.string().c_str(), 0); ASSERT_EQ(r, 0); git_repository_free(repo); } @@ -41,6 +42,11 @@ class GitUtilsTest : public ::testing::Test { return GitRepo::openRepo(tmpDir, true, false); } + + std::string getRepoName() const + { + return tmpDir.filename().string(); + } }; void writeString(CreateRegularFileSink & fileSink, std::string contents, bool executable) @@ -78,13 +84,13 @@ TEST_F(GitUtilsTest, sink_basic) // sink->createHardlink("foo-1.1/links/foo-2", CanonPath("foo-1.1/hello")); auto result = repo->dereferenceSingletonDirectory(sink->flush()); - auto accessor = repo->getAccessor(result, false); + auto accessor = repo->getAccessor(result, false, getRepoName()); auto entries = accessor->readDirectory(CanonPath::root); - ASSERT_EQ(entries.size(), 5); + ASSERT_EQ(entries.size(), 5u); ASSERT_EQ(accessor->readFile(CanonPath("hello")), "hello world"); ASSERT_EQ(accessor->readFile(CanonPath("bye")), "thanks for all the fish"); ASSERT_EQ(accessor->readLink(CanonPath("bye-link")), "bye"); - ASSERT_EQ(accessor->readDirectory(CanonPath("empty")).size(), 0); + ASSERT_EQ(accessor->readDirectory(CanonPath("empty")).size(), 0u); ASSERT_EQ(accessor->readFile(CanonPath("links/foo")), "hello world"); }; diff --git a/src/libfetchers-tests/meson.build b/src/libfetchers-tests/meson.build index fdab6ba6c41..33bc7f30eb1 100644 --- a/src/libfetchers-tests/meson.build +++ b/src/libfetchers-tests/meson.build @@ -4,8 +4,6 @@ project('nix-fetchers-tests', 'cpp', 'cpp_std=c++2a', # TODO(Qyriad): increase the warning level 'warning_level=1', - 'debug=true', - 'optimization=2', 'errorlogs=true', # Please print logs for tests that fail ], meson_version : '>= 1.1', @@ -14,18 +12,19 @@ project('nix-fetchers-tests', 'cpp', cxx = meson.get_compiler('cpp') -subdir('build-utils-meson/deps-lists') +subdir('nix-meson-build-support/deps-lists') deps_private_maybe_subproject = [ dependency('nix-store-test-support'), dependency('nix-fetchers'), + dependency('nix-fetchers-c'), ] deps_public_maybe_subproject = [ ] -subdir('build-utils-meson/subprojects') +subdir('nix-meson-build-support/subprojects') -subdir('build-utils-meson/export-all-symbols') -subdir('build-utils-meson/windows-version') +subdir('nix-meson-build-support/export-all-symbols') +subdir('nix-meson-build-support/windows-version') rapidcheck = dependency('rapidcheck') deps_private += rapidcheck @@ -33,18 +32,15 @@ deps_private += rapidcheck gtest = dependency('gtest', main : true) deps_private += gtest -add_project_arguments( - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. - '-include', 'config-util.hh', - '-include', 'config-store.hh', - # '-include', 'config-fetchers.h', - language : 'cpp', -) +libgit2 = dependency('libgit2') +deps_private += libgit2 -subdir('build-utils-meson/common') +subdir('nix-meson-build-support/common') sources = files( + 'access-tokens.cc', + 'git-utils.cc', + 'nix_api_fetchers.cc', 'public-key.cc', ) diff --git a/src/libfetchers-tests/nix-meson-build-support b/src/libfetchers-tests/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/libfetchers-tests/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/libfetchers-tests/nix_api_fetchers.cc b/src/libfetchers-tests/nix_api_fetchers.cc new file mode 100644 index 00000000000..8f3e6e3c583 --- /dev/null +++ b/src/libfetchers-tests/nix_api_fetchers.cc @@ -0,0 +1,18 @@ +#include "gmock/gmock.h" +#include + +#include "nix_api_fetchers.h" +#include "nix/store/tests/nix_api_store.hh" + +namespace nixC { + +TEST_F(nix_api_store_test, nix_api_fetchers_new_free) +{ + nix_fetchers_settings * settings = nix_fetchers_settings_new(ctx); + assert_ctx_ok(); + ASSERT_NE(nullptr, settings); + + nix_fetchers_settings_free(settings); +} + +} // namespace nixC diff --git a/src/libfetchers-tests/package.nix b/src/libfetchers-tests/package.nix index 7b2ba8f2cd8..48c1a07d885 100644 --- a/src/libfetchers-tests/package.nix +++ b/src/libfetchers-tests/package.nix @@ -1,19 +1,22 @@ -{ lib -, buildPackages -, stdenv -, mkMesonExecutable +{ + lib, + buildPackages, + stdenv, + mkMesonExecutable, -, nix-fetchers -, nix-store-test-support + nix-fetchers, + nix-fetchers-c, + nix-store-test-support, -, rapidcheck -, gtest -, runCommand + libgit2, + rapidcheck, + gtest, + runCommand, -# Configuration Options + # Configuration Options -, version -, resolvePath + version, + resolvePath, }: let @@ -26,8 +29,8 @@ mkMesonExecutable (finalAttrs: { workDir = ./.; fileset = fileset.unions [ - ../../build-utils-meson - ./build-utils-meson + ../../nix-meson-build-support + ./nix-meson-build-support ../../.version ./.version ./meson.build @@ -38,38 +41,34 @@ mkMesonExecutable (finalAttrs: { buildInputs = [ nix-fetchers + nix-fetchers-c nix-store-test-support rapidcheck gtest + libgit2 ]; - preConfigure = - # "Inline" .version so it's not a symlink, and includes the suffix. - # Do the meson utils, without modification. - '' - chmod u+w ./.version - echo ${version} > ../../.version - ''; - mesonFlags = [ ]; - env = lib.optionalAttrs (stdenv.isLinux && !(stdenv.hostPlatform.isStatic && stdenv.system == "aarch64-linux")) { - LDFLAGS = "-fuse-ld=gold"; - }; - passthru = { tests = { - run = runCommand "${finalAttrs.pname}-run" { - meta.broken = !stdenv.hostPlatform.emulatorAvailable buildPackages; - } (lib.optionalString stdenv.hostPlatform.isWindows '' - export HOME="$PWD/home-dir" - mkdir -p "$HOME" - '' + '' - export _NIX_TEST_UNIT_DATA=${resolvePath ./data} - ${stdenv.hostPlatform.emulator buildPackages} ${lib.getExe finalAttrs.finalPackage} - touch $out - ''); + run = + runCommand "${finalAttrs.pname}-run" + { + meta.broken = !stdenv.hostPlatform.emulatorAvailable buildPackages; + } + ( + lib.optionalString stdenv.hostPlatform.isWindows '' + export HOME="$PWD/home-dir" + mkdir -p "$HOME" + '' + + '' + export _NIX_TEST_UNIT_DATA=${resolvePath ./data} + ${stdenv.hostPlatform.emulator buildPackages} ${lib.getExe finalAttrs.finalPackage} + touch $out + '' + ); }; }; diff --git a/src/libfetchers-tests/public-key.cc b/src/libfetchers-tests/public-key.cc index 80796bd0fc9..39a7cf4bd09 100644 --- a/src/libfetchers-tests/public-key.cc +++ b/src/libfetchers-tests/public-key.cc @@ -1,8 +1,8 @@ #include -#include "fetchers.hh" -#include "json-utils.hh" +#include "nix/fetchers/fetchers.hh" +#include "nix/util/json-utils.hh" #include -#include "tests/characterization.hh" +#include "nix/util/tests/characterization.hh" namespace nix { diff --git a/src/libfetchers/attrs.cc b/src/libfetchers/attrs.cc index 25d04cdc950..6808e8af1f6 100644 --- a/src/libfetchers/attrs.cc +++ b/src/libfetchers/attrs.cc @@ -1,5 +1,5 @@ -#include "attrs.hh" -#include "fetchers.hh" +#include "nix/fetchers/attrs.hh" +#include "nix/fetchers/fetchers.hh" #include @@ -89,9 +89,9 @@ bool getBoolAttr(const Attrs & attrs, const std::string & name) return *s; } -std::map attrsToQuery(const Attrs & attrs) +StringMap attrsToQuery(const Attrs & attrs) { - std::map query; + StringMap query; for (auto & attr : attrs) { if (auto v = std::get_if(&attr.second)) { query.insert_or_assign(attr.first, fmt("%d", *v)); diff --git a/src/libfetchers/build-utils-meson b/src/libfetchers/build-utils-meson deleted file mode 120000 index 5fff21bab55..00000000000 --- a/src/libfetchers/build-utils-meson +++ /dev/null @@ -1 +0,0 @@ -../../build-utils-meson \ No newline at end of file diff --git a/src/libfetchers/cache.cc b/src/libfetchers/cache.cc index 6c2241f3af7..9a2531ba526 100644 --- a/src/libfetchers/cache.cc +++ b/src/libfetchers/cache.cc @@ -1,8 +1,9 @@ -#include "cache.hh" -#include "users.hh" -#include "sqlite.hh" -#include "sync.hh" -#include "store-api.hh" +#include "nix/fetchers/cache.hh" +#include "nix/fetchers/fetch-settings.hh" +#include "nix/util/users.hh" +#include "nix/store/sqlite.hh" +#include "nix/util/sync.hh" +#include "nix/store/store-api.hh" #include @@ -162,10 +163,12 @@ struct CacheImpl : Cache } }; -ref getCache() +ref Settings::getCache() const { - static auto cache = std::make_shared(); - return ref(cache); + auto cache(_cache.lock()); + if (!*cache) + *cache = std::make_shared(); + return ref(*cache); } } diff --git a/src/libfetchers/fetch-settings.cc b/src/libfetchers/fetch-settings.cc index c7ed4c7af08..4b4e4e29d98 100644 --- a/src/libfetchers/fetch-settings.cc +++ b/src/libfetchers/fetch-settings.cc @@ -1,4 +1,4 @@ -#include "fetch-settings.hh" +#include "nix/fetchers/fetch-settings.hh" namespace nix::fetchers { diff --git a/src/libfetchers/fetch-to-store.cc b/src/libfetchers/fetch-to-store.cc index fe347a59d5b..f7ab32322ef 100644 --- a/src/libfetchers/fetch-to-store.cc +++ b/src/libfetchers/fetch-to-store.cc @@ -1,10 +1,26 @@ -#include "fetch-to-store.hh" -#include "fetchers.hh" -#include "cache.hh" +#include "nix/fetchers/fetch-to-store.hh" +#include "nix/fetchers/fetchers.hh" +#include "nix/fetchers/fetch-settings.hh" namespace nix { +fetchers::Cache::Key makeFetchToStoreCacheKey( + const std::string & name, + const std::string & fingerprint, + ContentAddressMethod method, + const std::string & path) +{ + return fetchers::Cache::Key{"fetchToStore", { + {"name", name}, + {"fingerprint", fingerprint}, + {"method", std::string{method.render()}}, + {"path", path} + }}; + +} + StorePath fetchToStore( + const fetchers::Settings & settings, Store & store, const SourcePath & path, FetchMode mode, @@ -19,13 +35,8 @@ StorePath fetchToStore( std::optional cacheKey; if (!filter && path.accessor->fingerprint) { - cacheKey = fetchers::Cache::Key{"fetchToStore", { - {"name", std::string{name}}, - {"fingerprint", *path.accessor->fingerprint}, - {"method", std::string{method.render()}}, - {"path", path.path.abs()} - }}; - if (auto res = fetchers::getCache()->lookupStorePath(*cacheKey, store)) { + cacheKey = makeFetchToStoreCacheKey(std::string{name}, *path.accessor->fingerprint, method, path.path.abs()); + if (auto res = settings.getCache()->lookupStorePath(*cacheKey, store)) { debug("store path cache hit for '%s'", path); return res->storePath; } @@ -47,7 +58,7 @@ StorePath fetchToStore( debug(mode == FetchMode::DryRun ? "hashed '%s'" : "copied '%s' to '%s'", path, store.printStorePath(storePath)); if (cacheKey && mode == FetchMode::Copy) - fetchers::getCache()->upsert(*cacheKey, store, {}, storePath); + settings.getCache()->upsert(*cacheKey, store, {}, storePath); return storePath; } diff --git a/src/libfetchers/fetch-to-store.hh b/src/libfetchers/fetch-to-store.hh deleted file mode 100644 index c762629f3cb..00000000000 --- a/src/libfetchers/fetch-to-store.hh +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "source-path.hh" -#include "store-api.hh" -#include "file-system.hh" -#include "repair-flag.hh" -#include "file-content-address.hh" - -namespace nix { - -enum struct FetchMode { DryRun, Copy }; - -/** - * Copy the `path` to the Nix store. - */ -StorePath fetchToStore( - Store & store, - const SourcePath & path, - FetchMode mode, - std::string_view name = "source", - ContentAddressMethod method = ContentAddressMethod::Raw::NixArchive, - PathFilter * filter = nullptr, - RepairFlag repair = NoRepair); - -} diff --git a/src/libfetchers/fetchers.cc b/src/libfetchers/fetchers.cc index e15a460d0e4..9cb89660172 100644 --- a/src/libfetchers/fetchers.cc +++ b/src/libfetchers/fetchers.cc @@ -1,9 +1,10 @@ -#include "fetchers.hh" -#include "store-api.hh" -#include "source-path.hh" -#include "fetch-to-store.hh" -#include "json-utils.hh" -#include "store-path-accessor.hh" +#include "nix/fetchers/fetchers.hh" +#include "nix/store/store-api.hh" +#include "nix/util/source-path.hh" +#include "nix/fetchers/fetch-to-store.hh" +#include "nix/util/json-utils.hh" +#include "nix/fetchers/store-path-accessor.hh" +#include "nix/fetchers/fetch-settings.hh" #include @@ -11,24 +12,26 @@ namespace nix::fetchers { using InputSchemeMap = std::map>; -std::unique_ptr inputSchemes = nullptr; +static InputSchemeMap & inputSchemes() +{ + static InputSchemeMap inputSchemeMap; + return inputSchemeMap; +} void registerInputScheme(std::shared_ptr && inputScheme) { - if (!inputSchemes) - inputSchemes = std::make_unique(); auto schemeName = inputScheme->schemeName(); - if (inputSchemes->count(schemeName) > 0) + if (!inputSchemes().emplace(schemeName, std::move(inputScheme)).second) throw Error("Input scheme with name %s already registered", schemeName); - inputSchemes->insert_or_assign(schemeName, std::move(inputScheme)); } -nlohmann::json dumpRegisterInputSchemeInfo() { +nlohmann::json dumpRegisterInputSchemeInfo() +{ using nlohmann::json; auto res = json::object(); - for (auto & [name, scheme] : *inputSchemes) { + for (auto & [name, scheme] : inputSchemes()) { auto & r = res[name] = json::object(); r["allowedAttrs"] = scheme->allowedAttrs(); } @@ -56,7 +59,7 @@ Input Input::fromURL( const Settings & settings, const ParsedURL & url, bool requireTree) { - for (auto & [_, inputScheme] : *inputSchemes) { + for (auto & [_, inputScheme] : inputSchemes()) { auto res = inputScheme->inputFromURL(settings, url, requireTree); if (res) { experimentalFeatureSettings.require(inputScheme->experimentalFeature()); @@ -66,7 +69,7 @@ Input Input::fromURL( } } - throw Error("input '%s' is unsupported", url.url); + throw Error("input '%s' is unsupported", url); } Input Input::fromAttrs(const Settings & settings, Attrs && attrs) @@ -90,8 +93,8 @@ Input Input::fromAttrs(const Settings & settings, Attrs && attrs) }; std::shared_ptr inputScheme = ({ - auto i = inputSchemes->find(schemeName); - i == inputSchemes->end() ? nullptr : i->second; + auto i = get(inputSchemes(), schemeName); + i ? *i : nullptr; }); if (!inputScheme) return raw(); @@ -113,7 +116,15 @@ Input Input::fromAttrs(const Settings & settings, Attrs && attrs) std::optional Input::getFingerprint(ref store) const { - return scheme ? scheme->getFingerprint(store, *this) : std::nullopt; + if (!scheme) return std::nullopt; + + if (cachedFingerprint) return *cachedFingerprint; + + auto fingerprint = scheme->getFingerprint(store, *this); + + cachedFingerprint = fingerprint; + + return fingerprint; } ParsedURL Input::toURL() const @@ -123,7 +134,7 @@ ParsedURL Input::toURL() const return scheme->toURL(*this); } -std::string Input::toURLString(const std::map & extraQuery) const +std::string Input::toURLString(const StringMap & extraQuery) const { auto url = toURL(); for (auto & attr : extraQuery) @@ -151,6 +162,12 @@ bool Input::isFinal() const return maybeGetBoolAttr(attrs, "__final").value_or(false); } +std::optional Input::isRelative() const +{ + assert(scheme); + return scheme->isRelative(*this); +} + Attrs Input::toAttrs() const { return attrs; @@ -171,6 +188,7 @@ bool Input::contains(const Input & other) const return false; } +// FIXME: remove std::pair Input::fetchToStore(ref store) const { if (!scheme) @@ -180,15 +198,11 @@ std::pair Input::fetchToStore(ref store) const try { auto [accessor, result] = getAccessorUnchecked(store); - auto storePath = nix::fetchToStore(*store, SourcePath(accessor), FetchMode::Copy, result.getName()); + auto storePath = nix::fetchToStore(*settings, *store, SourcePath(accessor), FetchMode::Copy, result.getName()); auto narHash = store->queryPathInfo(storePath)->narHash; result.attrs.insert_or_assign("narHash", narHash.to_string(HashFormat::SRI, true)); - // FIXME: we would like to mark inputs as final in - // getAccessorUnchecked(), but then we can't add - // narHash. Or maybe narHash should be excluded from the - // concept of "final" inputs? result.attrs.insert_or_assign("__final", Explicit(true)); assert(result.isFinal()); @@ -269,6 +283,8 @@ std::pair, Input> Input::getAccessor(ref store) const try { auto [accessor, result] = getAccessorUnchecked(store); + result.attrs.insert_or_assign("__final", Explicit(true)); + checkLocks(*this, result); return {accessor, std::move(result)}; @@ -307,7 +323,9 @@ std::pair, Input> Input::getAccessorUnchecked(ref sto auto accessor = makeStorePathAccessor(store, storePath); - accessor->fingerprint = scheme->getFingerprint(store, *this); + accessor->fingerprint = getFingerprint(store); + + accessor->setPathDisplay("«" + to_string() + "»"); return {accessor, *this}; } catch (Error & e) { @@ -318,7 +336,7 @@ std::pair, Input> Input::getAccessorUnchecked(ref sto auto [accessor, result] = scheme->getAccessor(store, *this); assert(!accessor->fingerprint); - accessor->fingerprint = scheme->getFingerprint(store, result); + accessor->fingerprint = result.getFingerprint(store); return {accessor, std::move(result)}; } @@ -337,7 +355,7 @@ void Input::clone(const Path & destDir) const scheme->clone(*this, destDir); } -std::optional Input::getSourcePath() const +std::optional Input::getSourcePath() const { assert(scheme); return scheme->getSourcePath(*this); @@ -440,7 +458,7 @@ Input InputScheme::applyOverrides( return input; } -std::optional InputScheme::getSourcePath(const Input & input) const +std::optional InputScheme::getSourcePath(const Input & input) const { return {}; } diff --git a/src/libfetchers/filtering-source-accessor.cc b/src/libfetchers/filtering-source-accessor.cc index d4557b6d4dd..72a3fb4ebad 100644 --- a/src/libfetchers/filtering-source-accessor.cc +++ b/src/libfetchers/filtering-source-accessor.cc @@ -1,4 +1,4 @@ -#include "filtering-source-accessor.hh" +#include "nix/fetchers/filtering-source-accessor.hh" namespace nix { @@ -58,18 +58,23 @@ void FilteringSourceAccessor::checkAccess(const CanonPath & path) struct AllowListSourceAccessorImpl : AllowListSourceAccessor { std::set allowedPrefixes; + std::unordered_set allowedPaths; AllowListSourceAccessorImpl( ref next, std::set && allowedPrefixes, + std::unordered_set && allowedPaths, MakeNotAllowedError && makeNotAllowedError) : AllowListSourceAccessor(SourcePath(next), std::move(makeNotAllowedError)) , allowedPrefixes(std::move(allowedPrefixes)) + , allowedPaths(std::move(allowedPaths)) { } bool isAllowed(const CanonPath & path) override { - return path.isAllowed(allowedPrefixes); + return + allowedPaths.contains(path) + || path.isAllowed(allowedPrefixes); } void allowPrefix(CanonPath prefix) override @@ -81,9 +86,14 @@ struct AllowListSourceAccessorImpl : AllowListSourceAccessor ref AllowListSourceAccessor::create( ref next, std::set && allowedPrefixes, + std::unordered_set && allowedPaths, MakeNotAllowedError && makeNotAllowedError) { - return make_ref(next, std::move(allowedPrefixes), std::move(makeNotAllowedError)); + return make_ref( + next, + std::move(allowedPrefixes), + std::move(allowedPaths), + std::move(makeNotAllowedError)); } bool CachingFilteringSourceAccessor::isAllowed(const CanonPath & path) diff --git a/src/libfetchers/git-lfs-fetch.cc b/src/libfetchers/git-lfs-fetch.cc new file mode 100644 index 00000000000..97f10f0c6ec --- /dev/null +++ b/src/libfetchers/git-lfs-fetch.cc @@ -0,0 +1,280 @@ +#include "nix/fetchers/git-lfs-fetch.hh" +#include "nix/fetchers/git-utils.hh" +#include "nix/store/filetransfer.hh" +#include "nix/util/processes.hh" +#include "nix/util/url.hh" +#include "nix/util/users.hh" +#include "nix/util/hash.hh" + +#include +#include +#include +#include + +#include + +namespace nix::lfs { + +// if authHeader is "", downloadToSink assumes no auth is expected +static void downloadToSink( + const std::string & url, + const std::string & authHeader, + // FIXME: passing a StringSink is superfluous, we may as well + // return a string. Or use an abstract Sink for streaming. + StringSink & sink, + std::string sha256Expected, + size_t sizeExpected) +{ + FileTransferRequest request(url); + Headers headers; + if (!authHeader.empty()) + headers.push_back({"Authorization", authHeader}); + request.headers = headers; + getFileTransfer()->download(std::move(request), sink); + + auto sizeActual = sink.s.length(); + if (sizeExpected != sizeActual) + throw Error("size mismatch while fetching %s: expected %d but got %d", url, sizeExpected, sizeActual); + + auto sha256Actual = hashString(HashAlgorithm::SHA256, sink.s).to_string(HashFormat::Base16, false); + if (sha256Actual != sha256Expected) + throw Error( + "hash mismatch while fetching %s: expected sha256:%s but got sha256:%s", url, sha256Expected, sha256Actual); +} + +static std::string getLfsApiToken(const ParsedURL & url) +{ + auto [status, output] = runProgram( + RunOptions{ + .program = "ssh", + .args = {*url.authority, "git-lfs-authenticate", url.path, "download"}, + }); + + if (output.empty()) + throw Error( + "git-lfs-authenticate: no output (cmd: ssh %s git-lfs-authenticate %s download)", + url.authority.value_or(""), + url.path); + + auto queryResp = nlohmann::json::parse(output); + if (!queryResp.contains("header")) + throw Error("no header in git-lfs-authenticate response"); + if (!queryResp["header"].contains("Authorization")) + throw Error("no Authorization in git-lfs-authenticate response"); + + return queryResp["header"]["Authorization"].get(); +} + +typedef std::unique_ptr> GitConfig; +typedef std::unique_ptr> GitConfigEntry; + +static std::string getLfsEndpointUrl(git_repository * repo) +{ + GitConfig config; + if (git_repository_config(Setter(config), repo)) { + GitConfigEntry entry; + if (!git_config_get_entry(Setter(entry), config.get(), "lfs.url")) { + auto value = std::string(entry->value); + if (!value.empty()) { + debug("Found explicit lfs.url value: %s", value); + return value; + } + } + } + + git_remote * remote = nullptr; + if (git_remote_lookup(&remote, repo, "origin")) + return ""; + + const char * url_c_str = git_remote_url(remote); + if (!url_c_str) + return ""; + + return std::string(url_c_str); +} + +static std::optional parseLfsPointer(std::string_view content, std::string_view filename) +{ + // https://github.com/git-lfs/git-lfs/blob/2ef4108/docs/spec.md + // + // example git-lfs pointer file: + // version https://git-lfs.github.com/spec/v1 + // oid sha256:f5e02aa71e67f41d79023a128ca35bad86cf7b6656967bfe0884b3a3c4325eaf + // size 10000000 + // (ending \n) + + if (!content.starts_with("version ")) { + // Invalid pointer file + return std::nullopt; + } + + if (!content.starts_with("version https://git-lfs.github.com/spec/v1")) { + // In case there's new spec versions in the future, but for now only v1 exists + debug("Invalid version found on potential lfs pointer file, skipping"); + return std::nullopt; + } + + std::string oid; + std::string size; + + for (auto & line : tokenizeString(content, "\n")) { + if (line.starts_with("version ")) { + continue; + } + if (line.starts_with("oid sha256:")) { + oid = line.substr(11); // skip "oid sha256:" + continue; + } + if (line.starts_with("size ")) { + size = line.substr(5); // skip "size " + continue; + } + + debug("Custom extension '%s' found, ignoring", line); + } + + if (oid.length() != 64 || !std::all_of(oid.begin(), oid.end(), ::isxdigit)) { + debug("Invalid sha256 %s, skipping", oid); + return std::nullopt; + } + + if (size.length() == 0 || !std::all_of(size.begin(), size.end(), ::isdigit)) { + debug("Invalid size %s, skipping", size); + return std::nullopt; + } + + return std::make_optional(Pointer{oid, std::stoul(size)}); +} + +Fetch::Fetch(git_repository * repo, git_oid rev) +{ + this->repo = repo; + this->rev = rev; + + const auto remoteUrl = lfs::getLfsEndpointUrl(repo); + + this->url = nix::parseURL(nix::fixGitURL(remoteUrl)).canonicalise(); +} + +bool Fetch::shouldFetch(const CanonPath & path) const +{ + const char * attr = nullptr; + git_attr_options opts = GIT_ATTR_OPTIONS_INIT; + opts.attr_commit_id = this->rev; + opts.flags = GIT_ATTR_CHECK_INCLUDE_COMMIT | GIT_ATTR_CHECK_NO_SYSTEM; + if (git_attr_get_ext(&attr, (git_repository *) (this->repo), &opts, path.rel_c_str(), "filter")) + throw Error("cannot get git-lfs attribute: %s", git_error_last()->message); + debug("Git filter for '%s' is '%s'", path, attr ? attr : "null"); + return attr != nullptr && !std::string(attr).compare("lfs"); +} + +static nlohmann::json pointerToPayload(const std::vector & items) +{ + nlohmann::json jArray = nlohmann::json::array(); + for (const auto & pointer : items) + jArray.push_back({{"oid", pointer.oid}, {"size", pointer.size}}); + return jArray; +} + +std::vector Fetch::fetchUrls(const std::vector & pointers) const +{ + ParsedURL httpUrl(url); + httpUrl.scheme = url.scheme == "ssh" ? "https" : url.scheme; + FileTransferRequest request(httpUrl.to_string() + "/info/lfs/objects/batch"); + request.post = true; + Headers headers; + if (this->url.scheme == "ssh") + headers.push_back({"Authorization", lfs::getLfsApiToken(this->url)}); + headers.push_back({"Content-Type", "application/vnd.git-lfs+json"}); + headers.push_back({"Accept", "application/vnd.git-lfs+json"}); + request.headers = headers; + nlohmann::json oidList = pointerToPayload(pointers); + nlohmann::json data = {{"operation", "download"}}; + data["objects"] = oidList; + request.data = data.dump(); + + FileTransferResult result = getFileTransfer()->upload(request); + auto responseString = result.data; + + std::vector objects; + // example resp here: + // {"objects":[{"oid":"f5e02aa71e67f41d79023a128ca35bad86cf7b6656967bfe0884b3a3c4325eaf","size":10000000,"actions":{"download":{"href":"https://gitlab.com/b-camacho/test-lfs.git/gitlab-lfs/objects/f5e02aa71e67f41d79023a128ca35bad86cf7b6656967bfe0884b3a3c4325eaf","header":{"Authorization":"Basic + // Yi1jYW1hY2hvOmV5SjBlWEFpT2lKS1YxUWlMQ0poYkdjaU9pSklVekkxTmlKOS5leUprWVhSaElqcDdJbUZqZEc5eUlqb2lZaTFqWVcxaFkyaHZJbjBzSW1wMGFTSTZJbUptTURZNFpXVTFMVEprWmpVdE5HWm1ZUzFpWWpRMExUSXpNVEV3WVRReU1qWmtaaUlzSW1saGRDSTZNVGN4TkRZeE16ZzBOU3dpYm1KbUlqb3hOekUwTmpFek9EUXdMQ0psZUhBaU9qRTNNVFEyTWpFd05EVjkuZk9yMDNkYjBWSTFXQzFZaTBKRmJUNnJTTHJPZlBwVW9lYllkT0NQZlJ4QQ=="}}},"authenticated":true}]} + + try { + auto resp = nlohmann::json::parse(responseString); + if (resp.contains("objects")) + objects.insert(objects.end(), resp["objects"].begin(), resp["objects"].end()); + else + throw Error("response does not contain 'objects'"); + + return objects; + } catch (const nlohmann::json::parse_error & e) { + printMsg(lvlTalkative, "Full response: '%1%'", responseString); + throw Error("response did not parse as json: %s", e.what()); + } +} + +void Fetch::fetch( + const std::string & content, + const CanonPath & pointerFilePath, + StringSink & sink, + std::function sizeCallback) const +{ + debug("trying to fetch '%s' using git-lfs", pointerFilePath); + + if (content.length() >= 1024) { + warn("encountered file '%s' that should have been a git-lfs pointer, but is too large", pointerFilePath); + sizeCallback(content.length()); + sink(content); + return; + } + + const auto pointer = parseLfsPointer(content, pointerFilePath.rel()); + if (pointer == std::nullopt) { + warn("encountered file '%s' that should have been a git-lfs pointer, but is invalid", pointerFilePath); + sizeCallback(content.length()); + sink(content); + return; + } + + Path cacheDir = getCacheDir() + "/git-lfs"; + std::string key = hashString(HashAlgorithm::SHA256, pointerFilePath.rel()).to_string(HashFormat::Base16, false) + + "/" + pointer->oid; + Path cachePath = cacheDir + "/" + key; + if (pathExists(cachePath)) { + debug("using cache entry %s -> %s", key, cachePath); + sink(readFile(cachePath)); + return; + } + debug("did not find cache entry for %s", key); + + std::vector pointers; + pointers.push_back(pointer.value()); + const auto objUrls = fetchUrls(pointers); + + const auto obj = objUrls[0]; + try { + std::string sha256 = obj.at("oid"); // oid is also the sha256 + std::string ourl = obj.at("actions").at("download").at("href"); + std::string authHeader = ""; + if (obj.at("actions").at("download").contains("header") + && obj.at("actions").at("download").at("header").contains("Authorization")) { + authHeader = obj["actions"]["download"]["header"]["Authorization"]; + } + const uint64_t size = obj.at("size"); + sizeCallback(size); + downloadToSink(ourl, authHeader, sink, sha256, size); + + debug("creating cache entry %s -> %s", key, cachePath); + if (!pathExists(dirOf(cachePath))) + createDirs(dirOf(cachePath)); + writeFile(cachePath, sink.s); + + debug("%s fetched with git-lfs", pointerFilePath); + } catch (const nlohmann::json::out_of_range & e) { + throw Error("bad json from /info/lfs/objects/batch: %s %s", obj, e.what()); + } +} + +} // namespace nix::lfs diff --git a/src/libfetchers/git-utils.cc b/src/libfetchers/git-utils.cc index 74e68fe1281..9fe271fe8ce 100644 --- a/src/libfetchers/git-utils.cc +++ b/src/libfetchers/git-utils.cc @@ -1,10 +1,13 @@ -#include "git-utils.hh" -#include "cache.hh" -#include "finally.hh" -#include "processes.hh" -#include "signals.hh" -#include "users.hh" -#include "fs-sink.hh" +#include "nix/fetchers/git-utils.hh" +#include "nix/fetchers/git-lfs-fetch.hh" +#include "nix/fetchers/cache.hh" +#include "nix/fetchers/fetch-settings.hh" +#include "nix/util/finally.hh" +#include "nix/util/processes.hh" +#include "nix/util/signals.hh" +#include "nix/util/users.hh" +#include "nix/util/fs-sink.hh" +#include "nix/util/sync.hh" #include #include @@ -59,14 +62,6 @@ namespace nix { struct GitSourceAccessor; -// Some wrapper types that ensure that the git_*_free functions get called. -template -struct Deleter -{ - template - void operator()(T * p) const { del(p); }; -}; - typedef std::unique_ptr> Repository; typedef std::unique_ptr> TreeEntry; typedef std::unique_ptr> Tree; @@ -84,20 +79,6 @@ typedef std::unique_ptr> ObjectDb; typedef std::unique_ptr> PackBuilder; typedef std::unique_ptr> Indexer; -// A helper to ensure that we don't leak objects returned by libgit2. -template -struct Setter -{ - T & t; - typename T::pointer p = nullptr; - - Setter(T & t) : t(t) { } - - ~Setter() { if (p) t = T(p); } - - operator typename T::pointer * () { return &p; } -}; - Hash toHash(const git_oid & oid) { #ifdef GIT_EXPERIMENTAL_SHA256 @@ -205,7 +186,8 @@ static git_packbuilder_progress PACKBUILDER_PROGRESS_CHECK_INTERRUPT = &packBuil } // extern "C" -static void initRepoAtomically(std::filesystem::path &path, bool bare) { +static void initRepoAtomically(std::filesystem::path &path, bool bare) +{ if (pathExists(path.string())) return; Path tmpDir = createTempDir(os_string_to_string(PathViewNG { std::filesystem::path(path).parent_path() })); @@ -340,8 +322,17 @@ struct GitRepoImpl : GitRepo, std::enable_shared_from_this for (size_t n = 0; n < git_commit_parentcount(commit->get()); ++n) { git_commit * parent; - if (git_commit_parent(&parent, commit->get(), n)) - throw Error("getting parent of Git commit '%s': %s", *git_commit_id(commit->get()), git_error_last()->message); + if (git_commit_parent(&parent, commit->get(), n)) { + throw Error( + "Failed to retrieve the parent of Git commit '%s': %s. " + "This may be due to an incomplete repository history. " + "To resolve this, either enable the shallow parameter in your flake URL (?shallow=1) " + "or add set the shallow parameter to true in builtins.fetchGit, " + "or fetch the complete history for this branch.", + *git_commit_id(commit->get()), + git_error_last()->message + ); + } todo.push(Commit(parent)); } } @@ -386,7 +377,7 @@ struct GitRepoImpl : GitRepo, std::enable_shared_from_this if (git_config_iterator_glob_new(Setter(it), config.get(), "^submodule\\..*\\.(path|url|branch)$")) throw Error("iterating over .gitmodules: %s", git_error_last()->message); - std::map entries; + StringMap entries; while (true) { git_config_entry * entry = nullptr; @@ -437,7 +428,12 @@ struct GitRepoImpl : GitRepo, std::enable_shared_from_this { if (!(statusFlags & GIT_STATUS_INDEX_DELETED) && !(statusFlags & GIT_STATUS_WT_DELETED)) + { info.files.insert(CanonPath(path)); + if (statusFlags != GIT_STATUS_CURRENT) + info.dirtyFiles.insert(CanonPath(path)); + } else + info.deletedFiles.insert(CanonPath(path)); if (statusFlags != GIT_STATUS_CURRENT) info.isDirty = true; return 0; @@ -499,9 +495,15 @@ struct GitRepoImpl : GitRepo, std::enable_shared_from_this /** * A 'GitSourceAccessor' with no regard for export-ignore or any other transformations. */ - ref getRawAccessor(const Hash & rev); + ref getRawAccessor( + const Hash & rev, + bool smudgeLfs = false); - ref getAccessor(const Hash & rev, bool exportIgnore) override; + ref getAccessor( + const Hash & rev, + bool exportIgnore, + std::string displayPrefix, + bool smudgeLfs = false) override; ref getAccessor(const WorkdirInfo & wd, bool exportIgnore, MakeNotAllowedError e) override; @@ -538,13 +540,10 @@ struct GitRepoImpl : GitRepo, std::enable_shared_from_this // then use code that was removed in this commit (see blame) auto dir = this->path; - Strings gitArgs; - if (shallow) { - gitArgs = { "-C", dir.string(), "fetch", "--quiet", "--force", "--depth", "1", "--", url, refspec }; - } - else { - gitArgs = { "-C", dir.string(), "fetch", "--quiet", "--force", "--", url, refspec }; - } + Strings gitArgs{"-C", dir.string(), "--git-dir", ".", "fetch", "--quiet", "--force"}; + if (shallow) + append(gitArgs, {"--depth", "1"}); + append(gitArgs, {std::string("--"), url, refspec}); runProgram(RunOptions { .program = "git", @@ -597,7 +596,7 @@ struct GitRepoImpl : GitRepo, std::enable_shared_from_this }); /* Evaluate result through status code and checking if public - key fingerprints appear on stderr. This is neccessary + key fingerprints appear on stderr. This is necessary because the git command might also succeed due to the commit being signed by gpg keys that are present in the users key agent. */ @@ -621,18 +620,18 @@ struct GitRepoImpl : GitRepo, std::enable_shared_from_this throw Error("Commit signature verification on commit %s failed: %s", rev.gitRev(), output); } - Hash treeHashToNarHash(const Hash & treeHash) override + Hash treeHashToNarHash(const fetchers::Settings & settings, const Hash & treeHash) override { - auto accessor = getAccessor(treeHash, false); + auto accessor = getAccessor(treeHash, false, ""); fetchers::Cache::Key cacheKey{"treeHashToNarHash", {{"treeHash", treeHash.gitRev()}}}; - if (auto res = fetchers::getCache()->lookup(cacheKey)) + if (auto res = settings.getCache()->lookup(cacheKey)) return Hash::parseAny(fetchers::getStrAttr(*res, "narHash"), HashAlgorithm::SHA256); auto narHash = accessor->hashPath(CanonPath::root); - fetchers::getCache()->upsert(cacheKey, fetchers::Attrs({{"narHash", narHash.to_string(HashFormat::SRI, true)}})); + settings.getCache()->upsert(cacheKey, fetchers::Attrs({{"narHash", narHash.to_string(HashFormat::SRI, true)}})); return narHash; } @@ -663,24 +662,52 @@ ref GitRepo::openRepo(const std::filesystem::path & path, bool create, /** * Raw git tree input accessor. */ + struct GitSourceAccessor : SourceAccessor { - ref repo; - Object root; + struct State + { + ref repo; + Object root; + std::optional lfsFetch = std::nullopt; + }; - GitSourceAccessor(ref repo_, const Hash & rev) - : repo(repo_) - , root(peelToTreeOrBlob(lookupObject(*repo, hashToOID(rev)).get())) + Sync state_; + + GitSourceAccessor(ref repo_, const Hash & rev, bool smudgeLfs) + : state_{ + State { + .repo = repo_, + .root = peelToTreeOrBlob(lookupObject(*repo_, hashToOID(rev)).get()), + .lfsFetch = smudgeLfs ? std::make_optional(lfs::Fetch(*repo_, hashToOID(rev))) : std::nullopt, + } + } { } std::string readBlob(const CanonPath & path, bool symlink) { - auto blob = getBlob(path, symlink); - - auto data = std::string_view((const char *) git_blob_rawcontent(blob.get()), git_blob_rawsize(blob.get())); + auto state(state_.lock()); + + const auto blob = getBlob(*state, path, symlink); + + if (state->lfsFetch) { + if (state->lfsFetch->shouldFetch(path)) { + StringSink s; + try { + // FIXME: do we need to hold the state lock while + // doing this? + auto contents = std::string((const char *) git_blob_rawcontent(blob.get()), git_blob_rawsize(blob.get())); + state->lfsFetch->fetch(contents, path, s, [&s](uint64_t size){ s.s.reserve(size); }); + } catch (Error & e) { + e.addTrace({}, "while smudging git-lfs file '%s'", path); + throw; + } + return s.s; + } + } - return std::string(data); + return std::string((const char *) git_blob_rawcontent(blob.get()), git_blob_rawsize(blob.get())); } std::string readFile(const CanonPath & path) override @@ -690,15 +717,18 @@ struct GitSourceAccessor : SourceAccessor bool pathExists(const CanonPath & path) override { - return path.isRoot() ? true : (bool) lookup(path); + auto state(state_.lock()); + return path.isRoot() ? true : (bool) lookup(*state, path); } std::optional maybeLstat(const CanonPath & path) override { + auto state(state_.lock()); + if (path.isRoot()) - return Stat { .type = git_object_type(root.get()) == GIT_OBJECT_TREE ? tDirectory : tRegular }; + return Stat { .type = git_object_type(state->root.get()) == GIT_OBJECT_TREE ? tDirectory : tRegular }; - auto entry = lookup(path); + auto entry = lookup(*state, path); if (!entry) return std::nullopt; @@ -726,6 +756,8 @@ struct GitSourceAccessor : SourceAccessor DirEntries readDirectory(const CanonPath & path) override { + auto state(state_.lock()); + return std::visit(overloaded { [&](Tree tree) { DirEntries res; @@ -743,7 +775,7 @@ struct GitSourceAccessor : SourceAccessor [&](Submodule) { return DirEntries(); } - }, getTree(path)); + }, getTree(*state, path)); } std::string readLink(const CanonPath & path) override @@ -757,7 +789,9 @@ struct GitSourceAccessor : SourceAccessor */ std::optional getSubmoduleRev(const CanonPath & path) { - auto entry = lookup(path); + auto state(state_.lock()); + + auto entry = lookup(*state, path); if (!entry || git_tree_entry_type(entry) != GIT_OBJECT_COMMIT) return std::nullopt; @@ -768,7 +802,7 @@ struct GitSourceAccessor : SourceAccessor std::unordered_map lookupCache; /* Recursively look up 'path' relative to the root. */ - git_tree_entry * lookup(const CanonPath & path) + git_tree_entry * lookup(State & state, const CanonPath & path) { auto i = lookupCache.find(path); if (i != lookupCache.end()) return i->second.get(); @@ -778,7 +812,7 @@ struct GitSourceAccessor : SourceAccessor auto name = path.baseName().value(); - auto parentTree = lookupTree(*parent); + auto parentTree = lookupTree(state, *parent); if (!parentTree) return nullptr; auto count = git_tree_entrycount(parentTree->get()); @@ -807,29 +841,29 @@ struct GitSourceAccessor : SourceAccessor return res; } - std::optional lookupTree(const CanonPath & path) + std::optional lookupTree(State & state, const CanonPath & path) { if (path.isRoot()) { - if (git_object_type(root.get()) == GIT_OBJECT_TREE) - return dupObject((git_tree *) &*root); + if (git_object_type(state.root.get()) == GIT_OBJECT_TREE) + return dupObject((git_tree *) &*state.root); else return std::nullopt; } - auto entry = lookup(path); + auto entry = lookup(state, path); if (!entry || git_tree_entry_type(entry) != GIT_OBJECT_TREE) return std::nullopt; Tree tree; - if (git_tree_entry_to_object((git_object * *) (git_tree * *) Setter(tree), *repo, entry)) + if (git_tree_entry_to_object((git_object * *) (git_tree * *) Setter(tree), *state.repo, entry)) throw Error("looking up directory '%s': %s", showPath(path), git_error_last()->message); return tree; } - git_tree_entry * need(const CanonPath & path) + git_tree_entry * need(State & state, const CanonPath & path) { - auto entry = lookup(path); + auto entry = lookup(state, path); if (!entry) throw Error("'%s' does not exist", showPath(path)); return entry; @@ -837,16 +871,16 @@ struct GitSourceAccessor : SourceAccessor struct Submodule { }; - std::variant getTree(const CanonPath & path) + std::variant getTree(State & state, const CanonPath & path) { if (path.isRoot()) { - if (git_object_type(root.get()) == GIT_OBJECT_TREE) - return dupObject((git_tree *) &*root); + if (git_object_type(state.root.get()) == GIT_OBJECT_TREE) + return dupObject((git_tree *) &*state.root); else - throw Error("Git root object '%s' is not a directory", *git_object_id(root.get())); + throw Error("Git root object '%s' is not a directory", *git_object_id(state.root.get())); } - auto entry = need(path); + auto entry = need(state, path); if (git_tree_entry_type(entry) == GIT_OBJECT_COMMIT) return Submodule(); @@ -855,16 +889,16 @@ struct GitSourceAccessor : SourceAccessor throw Error("'%s' is not a directory", showPath(path)); Tree tree; - if (git_tree_entry_to_object((git_object * *) (git_tree * *) Setter(tree), *repo, entry)) + if (git_tree_entry_to_object((git_object * *) (git_tree * *) Setter(tree), *state.repo, entry)) throw Error("looking up directory '%s': %s", showPath(path), git_error_last()->message); return tree; } - Blob getBlob(const CanonPath & path, bool expectSymlink) + Blob getBlob(State & state, const CanonPath & path, bool expectSymlink) { - if (!expectSymlink && git_object_type(root.get()) == GIT_OBJECT_BLOB) - return dupObject((git_blob *) &*root); + if (!expectSymlink && git_object_type(state.root.get()) == GIT_OBJECT_BLOB) + return dupObject((git_blob *) &*state.root); auto notExpected = [&]() { @@ -877,7 +911,7 @@ struct GitSourceAccessor : SourceAccessor if (path.isRoot()) notExpected(); - auto entry = need(path); + auto entry = need(state, path); if (git_tree_entry_type(entry) != GIT_OBJECT_BLOB) notExpected(); @@ -892,7 +926,7 @@ struct GitSourceAccessor : SourceAccessor } Blob blob; - if (git_tree_entry_to_object((git_object * *) (git_blob * *) Setter(blob), *repo, entry)) + if (git_tree_entry_to_object((git_object * *) (git_blob * *) Setter(blob), *state.repo, entry)) throw Error("looking up file '%s': %s", showPath(path), git_error_last()->message); return blob; @@ -1184,37 +1218,38 @@ struct GitFileSystemObjectSinkImpl : GitFileSystemObjectSink } }; -ref GitRepoImpl::getRawAccessor(const Hash & rev) +ref GitRepoImpl::getRawAccessor( + const Hash & rev, + bool smudgeLfs) { auto self = ref(shared_from_this()); - return make_ref(self, rev); + return make_ref(self, rev, smudgeLfs); } -ref GitRepoImpl::getAccessor(const Hash & rev, bool exportIgnore) +ref GitRepoImpl::getAccessor( + const Hash & rev, + bool exportIgnore, + std::string displayPrefix, + bool smudgeLfs) { auto self = ref(shared_from_this()); - ref rawGitAccessor = getRawAccessor(rev); - if (exportIgnore) { + ref rawGitAccessor = getRawAccessor(rev, smudgeLfs); + rawGitAccessor->setPathDisplay(std::move(displayPrefix)); + if (exportIgnore) return make_ref(self, rawGitAccessor, rev); - } - else { + else return rawGitAccessor; - } } ref GitRepoImpl::getAccessor(const WorkdirInfo & wd, bool exportIgnore, MakeNotAllowedError makeNotAllowedError) { auto self = ref(shared_from_this()); - /* In case of an empty workdir, return an empty in-memory tree. We - cannot use AllowListSourceAccessor because it would return an - error for the root (and we can't add the root to the allow-list - since that would allow access to all its children). */ ref fileAccessor = - wd.files.empty() - ? makeEmptySourceAccessor() - : AllowListSourceAccessor::create( + AllowListSourceAccessor::create( makeFSSourceAccessor(path), - std::set { wd.files }, + std::set{ wd.files }, + // Always allow access to the root, but not its children. + std::unordered_set{CanonPath::root}, std::move(makeNotAllowedError)).cast(); if (exportIgnore) return make_ref(self, fileAccessor, std::nullopt); @@ -1232,14 +1267,19 @@ std::vector> GitRepoImpl::getSubmodules /* Read the .gitmodules files from this revision. */ CanonPath modulesFile(".gitmodules"); - auto accessor = getAccessor(rev, exportIgnore); + auto accessor = getAccessor(rev, exportIgnore, ""); if (!accessor->pathExists(modulesFile)) return {}; /* Parse it and get the revision of each submodule. */ auto configS = accessor->readFile(modulesFile); auto [fdTemp, pathTemp] = createTempFile("nix-git-submodules"); - writeFull(fdTemp.get(), configS); + try { + writeFull(fdTemp.get(), configS); + } catch (SysError & e) { + e.addTrace({}, "while writing .gitmodules file to temporary file"); + throw; + } std::vector> result; @@ -1262,4 +1302,17 @@ ref getTarballCache() return GitRepo::openRepo(repoDir, true, true); } +GitRepo::WorkdirInfo GitRepo::getCachedWorkdirInfo(const std::filesystem::path & path) +{ + static Sync> _cache; + { + auto cache(_cache.lock()); + auto i = cache->find(path); + if (i != cache->end()) return i->second; + } + auto workdirInfo = GitRepo::openRepo(path)->getWorkdirInfo(); + _cache.lock()->emplace(path, workdirInfo); + return workdirInfo; +} + } diff --git a/src/libfetchers/git.cc b/src/libfetchers/git.cc index a6883a2d355..cf255c00183 100644 --- a/src/libfetchers/git.cc +++ b/src/libfetchers/git.cc @@ -1,20 +1,20 @@ -#include "error.hh" -#include "fetchers.hh" -#include "users.hh" -#include "cache.hh" -#include "globals.hh" -#include "tarfile.hh" -#include "store-api.hh" -#include "url-parts.hh" -#include "pathlocks.hh" -#include "processes.hh" -#include "git.hh" -#include "mounted-source-accessor.hh" -#include "git-utils.hh" -#include "logging.hh" -#include "finally.hh" -#include "fetch-settings.hh" -#include "json-utils.hh" +#include "nix/util/error.hh" +#include "nix/fetchers/fetchers.hh" +#include "nix/util/users.hh" +#include "nix/fetchers/cache.hh" +#include "nix/store/globals.hh" +#include "nix/util/tarfile.hh" +#include "nix/store/store-api.hh" +#include "nix/util/url-parts.hh" +#include "nix/store/pathlocks.hh" +#include "nix/util/processes.hh" +#include "nix/util/git.hh" +#include "nix/fetchers/git-utils.hh" +#include "nix/util/logging.hh" +#include "nix/util/finally.hh" +#include "nix/fetchers/fetch-settings.hh" +#include "nix/util/json-utils.hh" +#include "nix/util/archive.hh" #include #include @@ -38,7 +38,7 @@ const std::string gitInitialBranch = "__nix_dummy_branch"; bool isCacheFileWithinTtl(time_t now, const struct stat & st) { - return st.st_mtime + settings.tarballTtl > now; + return st.st_mtime + static_cast(settings.tarballTtl) > now; } Path getCachePath(std::string_view key, bool shallow) @@ -68,7 +68,7 @@ std::optional readHead(const Path & path) std::string_view line = output; line = line.substr(0, line.find("\n")); - if (const auto parseResult = git::parseLsRemoteLine(line)) { + if (const auto parseResult = git::parseLsRemoteLine(line); parseResult && parseResult->reference == "HEAD") { switch (parseResult->kind) { case git::LsRemoteRefLine::Kind::Symbolic: debug("resolved HEAD ref '%s' for repo '%s'", parseResult->target, path); @@ -83,10 +83,9 @@ std::optional readHead(const Path & path) } // Persist the HEAD ref from the remote repo in the local cached repo. -bool storeCachedHead(const std::string & actualUrl, const std::string & headRef) +bool storeCachedHead(const std::string & actualUrl, bool shallow, const std::string & headRef) { - // set shallow=false as HEAD will never be queried for a shallow repo - Path cacheDir = getCachePath(actualUrl, false); + Path cacheDir = getCachePath(actualUrl, shallow); try { runProgram("git", true, { "-C", cacheDir, "--git-dir", ".", "symbolic-ref", "--", "HEAD", headRef }); } catch (ExecError &e) { @@ -105,12 +104,11 @@ bool storeCachedHead(const std::string & actualUrl, const std::string & headRef) return true; } -std::optional readHeadCached(const std::string & actualUrl) +std::optional readHeadCached(const std::string & actualUrl, bool shallow) { // Create a cache path to store the branch of the HEAD ref. Append something // in front of the URL to prevent collision with the repository itself. - // set shallow=false as HEAD will never be queried for a shallow repo - Path cacheDir = getCachePath(actualUrl, false); + Path cacheDir = getCachePath(actualUrl, shallow); Path headRefFile = cacheDir + "/HEAD"; time_t now = time(0); @@ -184,7 +182,7 @@ struct GitInputScheme : InputScheme for (auto & [name, value] : url.query) { if (name == "rev" || name == "ref" || name == "keytype" || name == "publicKey" || name == "publicKeys") attrs.emplace(name, value); - else if (name == "shallow" || name == "submodules" || name == "exportIgnore" || name == "allRefs" || name == "verifyCommit") + else if (name == "shallow" || name == "submodules" || name == "lfs" || name == "exportIgnore" || name == "allRefs" || name == "verifyCommit") attrs.emplace(name, Explicit { value == "1" }); else url2.query.emplace(name, value); @@ -209,6 +207,7 @@ struct GitInputScheme : InputScheme "rev", "shallow", "submodules", + "lfs", "exportIgnore", "lastModified", "revCount", @@ -261,6 +260,8 @@ struct GitInputScheme : InputScheme if (auto ref = input.getRef()) url.query.insert_or_assign("ref", *ref); if (getShallowAttr(input)) url.query.insert_or_assign("shallow", "1"); + if (getLfsAttr(input)) + url.query.insert_or_assign("lfs", "1"); if (getSubmodulesAttr(input)) url.query.insert_or_assign("submodules", "1"); if (maybeGetBoolAttr(input.attrs, "exportIgnore").value_or(false)) @@ -296,7 +297,7 @@ struct GitInputScheme : InputScheme Strings args = {"clone"}; - args.push_back(repoInfo.url); + args.push_back(repoInfo.locationToArg()); if (auto ref = input.getRef()) { args.push_back("--branch"); @@ -310,11 +311,9 @@ struct GitInputScheme : InputScheme runProgram("git", true, args, {}, true); } - std::optional getSourcePath(const Input & input) const override + std::optional getSourcePath(const Input & input) const override { - auto repoInfo = getRepoInfo(input); - if (repoInfo.isLocal) return repoInfo.url; - return std::nullopt; + return getRepoInfo(input).getPath(); } void putFile( @@ -324,14 +323,15 @@ struct GitInputScheme : InputScheme std::optional commitMsg) const override { auto repoInfo = getRepoInfo(input); - if (!repoInfo.isLocal) + auto repoPath = repoInfo.getPath(); + if (!repoPath) throw Error("cannot commit '%s' to Git repository '%s' because it's not a working tree", path, input.to_string()); - writeFile((CanonPath(repoInfo.url) / path).abs(), contents); + writeFile(*repoPath / path.rel(), contents); auto result = runProgram(RunOptions { .program = "git", - .args = {"-C", repoInfo.url, "--git-dir", repoInfo.gitDir, "check-ignore", "--quiet", std::string(path.rel())}, + .args = {"-C", repoPath->string(), "--git-dir", repoInfo.gitDir, "check-ignore", "--quiet", std::string(path.rel())}, }); auto exitCode = #ifndef WIN32 // TODO abstract over exit status handling on Windows @@ -344,15 +344,14 @@ struct GitInputScheme : InputScheme if (exitCode != 0) { // The path is not `.gitignore`d, we can add the file. runProgram("git", true, - { "-C", repoInfo.url, "--git-dir", repoInfo.gitDir, "add", "--intent-to-add", "--", std::string(path.rel()) }); + { "-C", repoPath->string(), "--git-dir", repoInfo.gitDir, "add", "--intent-to-add", "--", std::string(path.rel()) }); if (commitMsg) { // Pause the logger to allow for user input (such as a gpg passphrase) in `git commit` - logger->pause(); - Finally restoreLogger([]() { logger->resume(); }); + auto suspension = logger->suspend(); runProgram("git", true, - { "-C", repoInfo.url, "--git-dir", repoInfo.gitDir, "commit", std::string(path.rel()), "-F", "-" }, + { "-C", repoPath->string(), "--git-dir", repoInfo.gitDir, "commit", std::string(path.rel()), "-F", "-" }, *commitMsg); } } @@ -360,24 +359,41 @@ struct GitInputScheme : InputScheme struct RepoInfo { - /* Whether this is a local, non-bare repository. */ - bool isLocal = false; + /* Either the path of the repo (for local, non-bare repos), or + the URL (which is never a `file` URL). */ + std::variant location; /* Working directory info: the complete list of files, and whether the working directory is dirty compared to HEAD. */ GitRepo::WorkdirInfo workdirInfo; - /* URL of the repo, or its path if isLocal. Never a `file` URL. */ - std::string url; + std::string locationToArg() const + { + return std::visit( + overloaded { + [&](const std::filesystem::path & path) + { return path.string(); }, + [&](const ParsedURL & url) + { return url.to_string(); } + }, location); + } + + std::optional getPath() const + { + if (auto path = std::get_if(&location)) + return *path; + else + return std::nullopt; + } void warnDirty(const Settings & settings) const { if (workdirInfo.isDirty) { if (!settings.allowDirty) - throw Error("Git tree '%s' is dirty", url); + throw Error("Git tree '%s' is dirty", locationToArg()); if (settings.warnDirty) - warn("Git tree '%s' is dirty", url); + warn("Git tree '%s' is dirty", locationToArg()); } } @@ -394,6 +410,11 @@ struct GitInputScheme : InputScheme return maybeGetBoolAttr(input.attrs, "submodules").value_or(false); } + bool getLfsAttr(const Input & input) const + { + return maybeGetBoolAttr(input.attrs, "lfs").value_or(false); + } + bool getExportIgnoreAttr(const Input & input) const { return maybeGetBoolAttr(input.attrs, "exportIgnore").value_or(false); @@ -424,22 +445,46 @@ struct GitInputScheme : InputScheme static bool forceHttp = getEnv("_NIX_FORCE_HTTP") == "1"; // for testing auto url = parseURL(getStrAttr(input.attrs, "url")); bool isBareRepository = url.scheme == "file" && !pathExists(url.path + "/.git"); - repoInfo.isLocal = url.scheme == "file" && !forceHttp && !isBareRepository; - repoInfo.url = repoInfo.isLocal ? url.path : url.base; + // + // FIXME: here we turn a possibly relative path into an absolute path. + // This allows relative git flake inputs to be resolved against the + // **current working directory** (as in POSIX), which tends to work out + // ok in the context of flakes, but is the wrong behavior, + // as it should resolve against the flake.nix base directory instead. + // + // See: https://discourse.nixos.org/t/57783 and #9708 + // + if (url.scheme == "file" && !forceHttp && !isBareRepository) { + if (!isAbsolute(url.path)) { + warn( + "Fetching Git repository '%s', which uses a path relative to the current directory. " + "This is not supported and will stop working in a future release. " + "See https://github.com/NixOS/nix/issues/12281 for details.", + url); + } + repoInfo.location = std::filesystem::absolute(url.path); + } else { + if (url.scheme == "file") + /* Query parameters are meaningless for file://, but + Git interprets them as part of the file name. So get + rid of them. */ + url.query.clear(); + repoInfo.location = url; + } // If this is a local directory and no ref or revision is // given, then allow the use of an unclean working tree. - if (!input.getRef() && !input.getRev() && repoInfo.isLocal) - repoInfo.workdirInfo = GitRepo::openRepo(repoInfo.url)->getWorkdirInfo(); + if (auto repoPath = repoInfo.getPath(); !input.getRef() && !input.getRev() && repoPath) + repoInfo.workdirInfo = GitRepo::getCachedWorkdirInfo(*repoPath); return repoInfo; } - uint64_t getLastModified(const RepoInfo & repoInfo, const std::string & repoDir, const Hash & rev) const + uint64_t getLastModified(const Settings & settings, const RepoInfo & repoInfo, const std::filesystem::path & repoDir, const Hash & rev) const { Cache::Key key{"gitLastModified", {{"rev", rev.gitRev()}}}; - auto cache = getCache(); + auto cache = settings.getCache(); if (auto res = cache->lookup(key)) return getIntAttr(*res, "lastModified"); @@ -451,16 +496,16 @@ struct GitInputScheme : InputScheme return lastModified; } - uint64_t getRevCount(const RepoInfo & repoInfo, const std::string & repoDir, const Hash & rev) const + uint64_t getRevCount(const Settings & settings, const RepoInfo & repoInfo, const std::filesystem::path & repoDir, const Hash & rev) const { Cache::Key key{"gitRevCount", {{"rev", rev.gitRev()}}}; - auto cache = getCache(); + auto cache = settings.getCache(); if (auto revCountAttrs = cache->lookup(key)) return getIntAttr(*revCountAttrs, "revCount"); - Activity act(*logger, lvlChatty, actUnknown, fmt("getting Git revision count of '%s'", repoInfo.url)); + Activity act(*logger, lvlChatty, actUnknown, fmt("getting Git revision count of '%s'", repoInfo.locationToArg())); auto revCount = GitRepo::openRepo(repoDir)->getRevCount(rev); @@ -469,26 +514,36 @@ struct GitInputScheme : InputScheme return revCount; } - std::string getDefaultRef(const RepoInfo & repoInfo) const + std::string getDefaultRef(const RepoInfo & repoInfo, bool shallow) const { - auto head = repoInfo.isLocal - ? GitRepo::openRepo(repoInfo.url)->getWorkdirRef() - : readHeadCached(repoInfo.url); + auto head = std::visit( + overloaded { + [&](const std::filesystem::path & path) + { return GitRepo::openRepo(path)->getWorkdirRef(); }, + [&](const ParsedURL & url) + { return readHeadCached(url.to_string(), shallow); } + }, repoInfo.location); if (!head) { - warn("could not read HEAD ref from repo at '%s', using 'master'", repoInfo.url); + warn("could not read HEAD ref from repo at '%s', using 'master'", repoInfo.locationToArg()); return "master"; } return *head; } - static MakeNotAllowedError makeNotAllowedError(std::string url) + static MakeNotAllowedError makeNotAllowedError(std::filesystem::path repoPath) { - return [url{std::move(url)}](const CanonPath & path) -> RestrictedPathError - { - if (nix::pathExists(path.abs())) - return RestrictedPathError("access to path '%s' is forbidden because it is not under Git control; maybe you should 'git add' it to the repository '%s'?", path, url); + return [repoPath{std::move(repoPath)}](const CanonPath & path) -> RestrictedPathError { + if (pathExists(repoPath / path.rel())) + return RestrictedPathError( + "Path '%1%' in the repository %2% is not tracked by Git.\n" + "\n" + "To make it visible to Nix, run:\n" + "\n" + "git -C %2% add \"%1%\"", + path.rel(), + repoPath); else - return RestrictedPathError("path '%s' does not exist in Git repository '%s'", path, url); + return RestrictedPathError("Path '%s' does not exist in Git repository %s.", path.rel(), repoPath); }; } @@ -515,32 +570,34 @@ struct GitInputScheme : InputScheme auto origRev = input.getRev(); auto originalRef = input.getRef(); - auto ref = originalRef ? *originalRef : getDefaultRef(repoInfo); + bool shallow = getShallowAttr(input); + auto ref = originalRef ? *originalRef : getDefaultRef(repoInfo, shallow); input.attrs.insert_or_assign("ref", ref); - Path repoDir; + std::filesystem::path repoDir; - if (repoInfo.isLocal) { - repoDir = repoInfo.url; + if (auto repoPath = repoInfo.getPath()) { + repoDir = *repoPath; if (!input.getRev()) input.attrs.insert_or_assign("rev", GitRepo::openRepo(repoDir)->resolveRef(ref).gitRev()); } else { - Path cacheDir = getCachePath(repoInfo.url, getShallowAttr(input)); + auto repoUrl = std::get(repoInfo.location); + std::filesystem::path cacheDir = getCachePath(repoUrl.to_string(), shallow); repoDir = cacheDir; repoInfo.gitDir = "."; - createDirs(dirOf(cacheDir)); - PathLocks cacheDirLock({cacheDir}); + std::filesystem::create_directories(cacheDir.parent_path()); + PathLocks cacheDirLock({cacheDir.string()}); auto repo = GitRepo::openRepo(cacheDir, true, true); // We need to set the origin so resolving submodule URLs works - repo->setRemote("origin", repoInfo.url); + repo->setRemote("origin", repoUrl.to_string()); - Path localRefFile = + auto localRefFile = ref.compare(0, 5, "refs/") == 0 - ? cacheDir + "/" + ref - : cacheDir + "/refs/heads/" + ref; + ? cacheDir / ref + : cacheDir / "refs/heads" / ref; bool doFetch; time_t now = time(0); @@ -556,39 +613,40 @@ struct GitInputScheme : InputScheme /* If the local ref is older than ‘tarball-ttl’ seconds, do a git fetch to update the local ref to the remote ref. */ struct stat st; - doFetch = stat(localRefFile.c_str(), &st) != 0 || + doFetch = stat(localRefFile.string().c_str(), &st) != 0 || !isCacheFileWithinTtl(now, st); } } if (doFetch) { + bool shallow = getShallowAttr(input); try { auto fetchRef = getAllRefsAttr(input) - ? "refs/*" + ? "refs/*:refs/*" : input.getRev() ? input.getRev()->gitRev() : ref.compare(0, 5, "refs/") == 0 - ? ref + ? fmt("%1%:%1%", ref) : ref == "HEAD" ? ref - : "refs/heads/" + ref; + : fmt("%1%:%1%", "refs/heads/" + ref); - repo->fetch(repoInfo.url, fmt("%s:%s", fetchRef, fetchRef), getShallowAttr(input)); + repo->fetch(repoUrl.to_string(), fetchRef, shallow); } catch (Error & e) { - if (!pathExists(localRefFile)) throw; + if (!std::filesystem::exists(localRefFile)) throw; logError(e.info()); - warn("could not update local clone of Git repository '%s'; continuing with the most recent version", repoInfo.url); + warn("could not update local clone of Git repository '%s'; continuing with the most recent version", repoInfo.locationToArg()); } try { if (!input.getRev()) setWriteTime(localRefFile, now, now); } catch (Error & e) { - warn("could not update mtime for file '%s': %s", localRefFile, e.info().msg); + warn("could not update mtime for file %s: %s", localRefFile, e.info().msg); } - if (!originalRef && !storeCachedHead(repoInfo.url, ref)) - warn("could not update cached head '%s' for '%s'", ref, repoInfo.url); + if (!originalRef && !storeCachedHead(repoUrl.to_string(), shallow, ref)) + warn("could not update cached head '%s' for '%s'", ref, repoInfo.locationToArg()); } if (auto rev = input.getRev()) { @@ -600,8 +658,7 @@ struct GitInputScheme : InputScheme "allRefs = true;" ANSI_NORMAL " to " ANSI_BOLD "fetchGit" ANSI_NORMAL ".", rev->gitRev(), ref, - repoInfo.url - ); + repoInfo.locationToArg()); } else input.attrs.insert_or_assign("rev", repo->resolveRef(ref).gitRev()); @@ -613,7 +670,7 @@ struct GitInputScheme : InputScheme auto isShallow = repo->isShallow(); if (isShallow && !getShallowAttr(input)) - throw Error("'%s' is a shallow Git repository, but shallow repositories are only allowed when `shallow = true;` is specified", repoInfo.url); + throw Error("'%s' is a shallow Git repository, but shallow repositories are only allowed when `shallow = true;` is specified", repoInfo.locationToArg()); // FIXME: check whether rev is an ancestor of ref? @@ -621,21 +678,20 @@ struct GitInputScheme : InputScheme Attrs infoAttrs({ {"rev", rev.gitRev()}, - {"lastModified", getLastModified(repoInfo, repoDir, rev)}, + {"lastModified", getLastModified(*input.settings, repoInfo, repoDir, rev)}, }); if (!getShallowAttr(input)) infoAttrs.insert_or_assign("revCount", - getRevCount(repoInfo, repoDir, rev)); + getRevCount(*input.settings, repoInfo, repoDir, rev)); - printTalkative("using revision %s of repo '%s'", rev.gitRev(), repoInfo.url); + printTalkative("using revision %s of repo '%s'", rev.gitRev(), repoInfo.locationToArg()); verifyCommit(input, repo); bool exportIgnore = getExportIgnoreAttr(input); - auto accessor = repo->getAccessor(rev, exportIgnore); - - accessor->setPathDisplay("«" + input.to_string() + "»"); + bool smudgeLfs = getLfsAttr(input); + auto accessor = repo->getAccessor(rev, exportIgnore, "«" + input.to_string() + "»", smudgeLfs); /* If the repo has submodules, fetch them and return a mounted input accessor consisting of the accessor for the top-level @@ -655,6 +711,7 @@ struct GitInputScheme : InputScheme attrs.insert_or_assign("rev", submoduleRev.gitRev()); attrs.insert_or_assign("exportIgnore", Explicit{ exportIgnore }); attrs.insert_or_assign("submodules", Explicit{ true }); + attrs.insert_or_assign("lfs", Explicit{ smudgeLfs }); attrs.insert_or_assign("allRefs", Explicit{ true }); auto submoduleInput = fetchers::Input::fromAttrs(*input.settings, std::move(attrs)); auto [submoduleAccessor, submoduleInput2] = @@ -682,21 +739,21 @@ struct GitInputScheme : InputScheme RepoInfo & repoInfo, Input && input) const { + auto repoPath = repoInfo.getPath().value(); + if (getSubmodulesAttr(input)) /* Create mountpoints for the submodules. */ for (auto & submodule : repoInfo.workdirInfo.submodules) repoInfo.workdirInfo.files.insert(submodule.path); - auto repo = GitRepo::openRepo(repoInfo.url, false, false); + auto repo = GitRepo::openRepo(repoPath, false, false); auto exportIgnore = getExportIgnoreAttr(input); ref accessor = repo->getAccessor(repoInfo.workdirInfo, exportIgnore, - makeNotAllowedError(repoInfo.url)); - - accessor->setPathDisplay(repoInfo.url); + makeNotAllowedError(repoPath)); /* If the repo has submodules, return a mounted input accessor consisting of the accessor for the top-level repo and the @@ -705,10 +762,10 @@ struct GitInputScheme : InputScheme std::map> mounts; for (auto & submodule : repoInfo.workdirInfo.submodules) { - auto submodulePath = CanonPath(repoInfo.url) / submodule.path; + auto submodulePath = repoPath / submodule.path.rel(); fetchers::Attrs attrs; attrs.insert_or_assign("type", "git"); - attrs.insert_or_assign("url", submodulePath.abs()); + attrs.insert_or_assign("url", submodulePath.string()); attrs.insert_or_assign("exportIgnore", Explicit{ exportIgnore }); attrs.insert_or_assign("submodules", Explicit{ true }); // TODO: fall back to getAccessorFromCommit-like fetch when submodules aren't checked out @@ -732,7 +789,7 @@ struct GitInputScheme : InputScheme } if (!repoInfo.workdirInfo.isDirty) { - auto repo = GitRepo::openRepo(repoInfo.url); + auto repo = GitRepo::openRepo(repoPath); if (auto ref = repo->getWorkdirRef()) input.attrs.insert_or_assign("ref", *ref); @@ -741,8 +798,10 @@ struct GitInputScheme : InputScheme auto rev = repoInfo.workdirInfo.headRev.value_or(nullRev); input.attrs.insert_or_assign("rev", rev.gitRev()); - input.attrs.insert_or_assign("revCount", - rev == nullRev ? 0 : getRevCount(repoInfo, repoInfo.url, rev)); + if (!getShallowAttr(input)) { + input.attrs.insert_or_assign("revCount", + rev == nullRev ? 0 : getRevCount(*input.settings, repoInfo, repoPath, rev)); + } verifyCommit(input, repo); } else { @@ -761,7 +820,7 @@ struct GitInputScheme : InputScheme input.attrs.insert_or_assign( "lastModified", repoInfo.workdirInfo.headRev - ? getLastModified(repoInfo, repoInfo.url, *repoInfo.workdirInfo.headRev) + ? getLastModified(*input.settings, repoInfo, repoPath, *repoInfo.workdirInfo.headRev) : 0); return {accessor, std::move(input)}; @@ -784,7 +843,7 @@ struct GitInputScheme : InputScheme } auto [accessor, final] = - input.getRef() || input.getRev() || !repoInfo.isLocal + input.getRef() || input.getRev() || !repoInfo.getPath() ? getAccessorFromCommit(store, repoInfo, std::move(input)) : getAccessorFromWorkdir(store, repoInfo, std::move(input)); @@ -793,15 +852,39 @@ struct GitInputScheme : InputScheme std::optional getFingerprint(ref store, const Input & input) const override { + auto makeFingerprint = [&](const Hash & rev) + { + return rev.gitRev() + (getSubmodulesAttr(input) ? ";s" : "") + (getExportIgnoreAttr(input) ? ";e" : "") + (getLfsAttr(input) ? ";l" : ""); + }; + if (auto rev = input.getRev()) - return rev->gitRev() + (getSubmodulesAttr(input) ? ";s" : "") + (getExportIgnoreAttr(input) ? ";e" : ""); - else + return makeFingerprint(*rev); + else { + auto repoInfo = getRepoInfo(input); + if (auto repoPath = repoInfo.getPath(); repoPath && repoInfo.workdirInfo.headRev && repoInfo.workdirInfo.submodules.empty()) { + /* Calculate a fingerprint that takes into account the + deleted and modified/added files. */ + HashSink hashSink{HashAlgorithm::SHA512}; + for (auto & file : repoInfo.workdirInfo.dirtyFiles) { + writeString("modified:", hashSink); + writeString(file.abs(), hashSink); + dumpPath((*repoPath / file.rel()).string(), hashSink); + } + for (auto & file : repoInfo.workdirInfo.deletedFiles) { + writeString("deleted:", hashSink); + writeString(file.abs(), hashSink); + } + return makeFingerprint(*repoInfo.workdirInfo.headRev) + + ";d=" + hashSink.finish().first.to_string(HashFormat::Base16, false); + } return std::nullopt; + } } bool isLocked(const Input & input) const override { - return (bool) input.getRev(); + auto rev = input.getRev(); + return rev && rev != nullRev; } }; diff --git a/src/libfetchers/github.cc b/src/libfetchers/github.cc index 308cff33a46..7a902d816d0 100644 --- a/src/libfetchers/github.cc +++ b/src/libfetchers/github.cc @@ -1,15 +1,15 @@ -#include "filetransfer.hh" -#include "cache.hh" -#include "globals.hh" -#include "store-api.hh" -#include "types.hh" -#include "url-parts.hh" -#include "git.hh" -#include "fetchers.hh" -#include "fetch-settings.hh" -#include "tarball.hh" -#include "tarfile.hh" -#include "git-utils.hh" +#include "nix/store/filetransfer.hh" +#include "nix/fetchers/cache.hh" +#include "nix/store/globals.hh" +#include "nix/store/store-api.hh" +#include "nix/util/types.hh" +#include "nix/util/url-parts.hh" +#include "nix/util/git.hh" +#include "nix/fetchers/fetchers.hh" +#include "nix/fetchers/fetch-settings.hh" +#include "nix/fetchers/tarball.hh" +#include "nix/util/tarfile.hh" +#include "nix/fetchers/git-utils.hh" #include #include @@ -50,7 +50,7 @@ struct GitArchiveInputScheme : InputScheme else if (std::regex_match(path[2], refRegex)) ref = path[2]; else - throw BadURL("in URL '%s', '%s' is not a commit hash or branch/tag name", url.url, path[2]); + throw BadURL("in URL '%s', '%s' is not a commit hash or branch/tag name", url, path[2]); } else if (size > 3) { std::string rs; for (auto i = std::next(path.begin(), 2); i != path.end(); i++) { @@ -63,34 +63,34 @@ struct GitArchiveInputScheme : InputScheme if (std::regex_match(rs, refRegex)) { ref = rs; } else { - throw BadURL("in URL '%s', '%s' is not a branch/tag name", url.url, rs); + throw BadURL("in URL '%s', '%s' is not a branch/tag name", url, rs); } } else if (size < 2) - throw BadURL("URL '%s' is invalid", url.url); + throw BadURL("URL '%s' is invalid", url); for (auto &[name, value] : url.query) { if (name == "rev") { if (rev) - throw BadURL("URL '%s' contains multiple commit hashes", url.url); + throw BadURL("URL '%s' contains multiple commit hashes", url); rev = Hash::parseAny(value, HashAlgorithm::SHA1); } else if (name == "ref") { if (!std::regex_match(value, refRegex)) - throw BadURL("URL '%s' contains an invalid branch/tag name", url.url); + throw BadURL("URL '%s' contains an invalid branch/tag name", url); if (ref) - throw BadURL("URL '%s' contains multiple branch/tag names", url.url); + throw BadURL("URL '%s' contains multiple branch/tag names", url); ref = value; } else if (name == "host") { if (!std::regex_match(value, hostRegex)) - throw BadURL("URL '%s' contains an invalid instance host", url.url); + throw BadURL("URL '%s' contains an invalid instance host", url); host_url = value; } // FIXME: barf on unsupported attributes } if (ref && rev) - throw BadURL("URL '%s' contains both a commit hash and a branch/tag name %s %s", url.url, *ref, rev->gitRev()); + throw BadURL("URL '%s' contains both a commit hash and a branch/tag name %s %s", url, *ref, rev->gitRev()); Input input{settings}; input.attrs.insert_or_assign("type", std::string { schemeName() }); @@ -149,6 +149,9 @@ struct GitArchiveInputScheme : InputScheme }; if (auto narHash = input.getNarHash()) url.query.insert_or_assign("narHash", narHash->to_string(HashFormat::SRI, true)); + auto host = maybeGetStrAttr(input.attrs, "host"); + if (host) + url.query.insert_or_assign("host", *host); return url; } @@ -172,9 +175,30 @@ struct GitArchiveInputScheme : InputScheme return input; } - std::optional getAccessToken(const fetchers::Settings & settings, const std::string & host) const + // Search for the longest possible match starting from the beginning and ending at either the end or a path segment. + std::optional getAccessToken(const fetchers::Settings & settings, const std::string & host, const std::string & url) const override { auto tokens = settings.accessTokens.get(); + std::string answer; + size_t answer_match_len = 0; + if(! url.empty()) { + for (auto & token : tokens) { + auto first = url.find(token.first); + if ( + first != std::string::npos + && token.first.length() > answer_match_len + && first == 0 + && url.substr(0,token.first.length()) == token.first + && (url.length() == token.first.length() || url[token.first.length()] == '/') + ) + { + answer = token.second; + answer_match_len = token.first.length(); + } + } + if (!answer.empty()) + return answer; + } if (auto token = get(tokens, host)) return *token; return {}; @@ -182,10 +206,22 @@ struct GitArchiveInputScheme : InputScheme Headers makeHeadersWithAuthTokens( const fetchers::Settings & settings, - const std::string & host) const + const std::string & host, + const Input & input) const + { + auto owner = getStrAttr(input.attrs, "owner"); + auto repo = getStrAttr(input.attrs, "repo"); + auto hostAndPath = fmt( "%s/%s/%s", host, owner, repo); + return makeHeadersWithAuthTokens(settings, host, hostAndPath); + } + + Headers makeHeadersWithAuthTokens( + const fetchers::Settings & settings, + const std::string & host, + const std::string & hostAndPath) const { Headers headers; - auto accessToken = getAccessToken(settings, host); + auto accessToken = getAccessToken(settings, host, hostAndPath); if (accessToken) { auto hdr = accessHeaderFromToken(*accessToken); if (hdr) @@ -229,7 +265,7 @@ struct GitArchiveInputScheme : InputScheme input.attrs.erase("ref"); input.attrs.insert_or_assign("rev", rev->gitRev()); - auto cache = getCache(); + auto cache = input.settings->getCache(); Cache::Key treeHashKey{"gitRevToTreeHash", {{"rev", rev->gitRev()}}}; Cache::Key lastModifiedKey{"gitRevToLastModified", {{"rev", rev->gitRev()}}}; @@ -294,9 +330,10 @@ struct GitArchiveInputScheme : InputScheme #endif input.attrs.insert_or_assign("lastModified", uint64_t(tarballInfo.lastModified)); - auto accessor = getTarballCache()->getAccessor(tarballInfo.treeHash, false); - - accessor->setPathDisplay("«" + input.to_string() + "»"); + auto accessor = getTarballCache()->getAccessor( + tarballInfo.treeHash, + false, + "«" + input.to_string() + "»"); return {accessor, input}; } @@ -365,12 +402,12 @@ struct GitHubInputScheme : GitArchiveInputScheme : "https://%s/api/v3/repos/%s/%s/commits/%s", host, getOwner(input), getRepo(input), *input.getRef()); - Headers headers = makeHeadersWithAuthTokens(*input.settings, host); + Headers headers = makeHeadersWithAuthTokens(*input.settings, host, input); auto json = nlohmann::json::parse( readFile( store->toRealPath( - downloadFile(store, url, "source", headers).storePath))); + downloadFile(store, *input.settings, url, "source", headers).storePath))); return RefInfo { .rev = Hash::parseAny(std::string { json["sha"] }, HashAlgorithm::SHA1), @@ -382,7 +419,7 @@ struct GitHubInputScheme : GitArchiveInputScheme { auto host = getHost(input); - Headers headers = makeHeadersWithAuthTokens(*input.settings, host); + Headers headers = makeHeadersWithAuthTokens(*input.settings, host, input); // If we have no auth headers then we default to the public archive // urls so we do not run into rate limits. @@ -439,12 +476,12 @@ struct GitLabInputScheme : GitArchiveInputScheme auto url = fmt("https://%s/api/v4/projects/%s%%2F%s/repository/commits?ref_name=%s", host, getStrAttr(input.attrs, "owner"), getStrAttr(input.attrs, "repo"), *input.getRef()); - Headers headers = makeHeadersWithAuthTokens(*input.settings, host); + Headers headers = makeHeadersWithAuthTokens(*input.settings, host, input); auto json = nlohmann::json::parse( readFile( store->toRealPath( - downloadFile(store, url, "source", headers).storePath))); + downloadFile(store, *input.settings, url, "source", headers).storePath))); if (json.is_array() && json.size() >= 1 && json[0]["id"] != nullptr) { return RefInfo { @@ -469,7 +506,7 @@ struct GitLabInputScheme : GitArchiveInputScheme host, getStrAttr(input.attrs, "owner"), getStrAttr(input.attrs, "repo"), input.getRev()->to_string(HashFormat::Base16, false)); - Headers headers = makeHeadersWithAuthTokens(*input.settings, host); + Headers headers = makeHeadersWithAuthTokens(*input.settings, host, input); return DownloadUrl { url, headers }; } @@ -509,12 +546,12 @@ struct SourceHutInputScheme : GitArchiveInputScheme auto base_url = fmt("https://%s/%s/%s", host, getStrAttr(input.attrs, "owner"), getStrAttr(input.attrs, "repo")); - Headers headers = makeHeadersWithAuthTokens(*input.settings, host); + Headers headers = makeHeadersWithAuthTokens(*input.settings, host, input); std::string refUri; if (ref == "HEAD") { auto file = store->toRealPath( - downloadFile(store, fmt("%s/HEAD", base_url), "source", headers).storePath); + downloadFile(store, *input.settings, fmt("%s/HEAD", base_url), "source", headers).storePath); std::ifstream is(file); std::string line; getline(is, line); @@ -530,7 +567,7 @@ struct SourceHutInputScheme : GitArchiveInputScheme std::regex refRegex(refUri); auto file = store->toRealPath( - downloadFile(store, fmt("%s/info/refs", base_url), "source", headers).storePath); + downloadFile(store, *input.settings, fmt("%s/info/refs", base_url), "source", headers).storePath); std::ifstream is(file); std::string line; @@ -556,7 +593,7 @@ struct SourceHutInputScheme : GitArchiveInputScheme host, getStrAttr(input.attrs, "owner"), getStrAttr(input.attrs, "repo"), input.getRev()->to_string(HashFormat::Base16, false)); - Headers headers = makeHeadersWithAuthTokens(*input.settings, host); + Headers headers = makeHeadersWithAuthTokens(*input.settings, host, input); return DownloadUrl { url, headers }; } diff --git a/src/libfetchers/attrs.hh b/src/libfetchers/include/nix/fetchers/attrs.hh similarity index 90% rename from src/libfetchers/attrs.hh rename to src/libfetchers/include/nix/fetchers/attrs.hh index 97a74bce013..582abd14413 100644 --- a/src/libfetchers/attrs.hh +++ b/src/libfetchers/include/nix/fetchers/attrs.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "types.hh" -#include "hash.hh" +#include "nix/util/types.hh" +#include "nix/util/hash.hh" #include @@ -37,7 +37,7 @@ std::optional maybeGetBoolAttr(const Attrs & attrs, const std::string & na bool getBoolAttr(const Attrs & attrs, const std::string & name); -std::map attrsToQuery(const Attrs & attrs); +StringMap attrsToQuery(const Attrs & attrs); Hash getRevAttr(const Attrs & attrs, const std::string & name); diff --git a/src/libfetchers/cache.hh b/src/libfetchers/include/nix/fetchers/cache.hh similarity index 96% rename from src/libfetchers/cache.hh rename to src/libfetchers/include/nix/fetchers/cache.hh index 4d834fe0ca3..6ac693183f9 100644 --- a/src/libfetchers/cache.hh +++ b/src/libfetchers/include/nix/fetchers/cache.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "fetchers.hh" -#include "path.hh" +#include "nix/fetchers/fetchers.hh" +#include "nix/store/path.hh" namespace nix::fetchers { @@ -91,6 +91,4 @@ struct Cache Store & store) = 0; }; -ref getCache(); - } diff --git a/src/libfetchers/fetch-settings.hh b/src/libfetchers/include/nix/fetchers/fetch-settings.hh similarity index 71% rename from src/libfetchers/fetch-settings.hh rename to src/libfetchers/include/nix/fetchers/fetch-settings.hh index f7cb34a0220..9cfd25e0b83 100644 --- a/src/libfetchers/fetch-settings.hh +++ b/src/libfetchers/include/nix/fetchers/fetch-settings.hh @@ -1,8 +1,10 @@ #pragma once ///@file -#include "types.hh" -#include "config.hh" +#include "nix/util/types.hh" +#include "nix/util/configuration.hh" +#include "nix/util/ref.hh" +#include "nix/util/sync.hh" #include #include @@ -11,6 +13,8 @@ namespace nix::fetchers { +struct Cache; + struct Settings : public Config { Settings(); @@ -23,9 +27,11 @@ struct Settings : public Config Access tokens are specified as a string made up of space-separated `host=token` values. The specific token used is selected by matching the `host` portion against the - "host" specification of the input. The actual use of the - `token` value is determined by the type of resource being - accessed: + "host" specification of the input. The `host` portion may + contain a path element which matches against the prefix + URL for the input. (eg: `github.com/org=token`). The actual use + of the `token` value is determined by the type of resource + being accessed: * Github: the token value is the OAUTH-TOKEN string obtained as the Personal Access Token from the Github server (see @@ -70,14 +76,30 @@ struct Settings : public Config Setting warnDirty{this, true, "warn-dirty", "Whether to warn about dirty Git/Mercurial trees."}; + Setting allowDirtyLocks{ + this, + false, + "allow-dirty-locks", + R"( + Whether to allow dirty inputs (such as dirty Git workdirs) + to be locked via their NAR hash. This is generally bad + practice since Nix has no way to obtain such inputs if they + are subsequently modified. Therefore lock files with dirty + locks should generally only be used for local testing, and + should not be pushed to other users. + )", + {}, + true, + Xp::Flakes}; + Setting trustTarballsFromGitForges{ this, true, "trust-tarballs-from-git-forges", R"( - If enabled (the default), Nix will consider tarballs from + If enabled (the default), Nix considers tarballs from GitHub and similar Git forges to be locked if a Git revision is specified, e.g. `github:NixOS/patchelf/7c2f768bf9601268a4e71c2ebe91e2011918a70f`. - This requires Nix to trust that the provider will return the + This requires Nix to trust that the provider returns the correct contents for the specified Git revision. If disabled, such tarballs are only considered locked if a @@ -92,6 +114,11 @@ struct Settings : public Config When empty, disables the global flake registry. )", {}, true, Xp::Flakes}; + + ref getCache() const; + +private: + mutable Sync> _cache; }; } diff --git a/src/libfetchers/include/nix/fetchers/fetch-to-store.hh b/src/libfetchers/include/nix/fetchers/fetch-to-store.hh new file mode 100644 index 00000000000..a52d567ecfb --- /dev/null +++ b/src/libfetchers/include/nix/fetchers/fetch-to-store.hh @@ -0,0 +1,30 @@ +#pragma once + +#include "nix/util/source-path.hh" +#include "nix/store/store-api.hh" +#include "nix/util/file-system.hh" +#include "nix/util/repair-flag.hh" +#include "nix/util/file-content-address.hh" +#include "nix/fetchers/cache.hh" + +namespace nix { + +enum struct FetchMode { DryRun, Copy }; + +/** + * Copy the `path` to the Nix store. + */ +StorePath fetchToStore( + const fetchers::Settings & settings, + Store & store, + const SourcePath & path, + FetchMode mode, + std::string_view name = "source", + ContentAddressMethod method = ContentAddressMethod::Raw::NixArchive, + PathFilter * filter = nullptr, + RepairFlag repair = NoRepair); + +fetchers::Cache::Key makeFetchToStoreCacheKey( + const std::string & name, const std::string & fingerprint, ContentAddressMethod method, const std::string & path); + +} diff --git a/src/libfetchers/fetchers.hh b/src/libfetchers/include/nix/fetchers/fetchers.hh similarity index 87% rename from src/libfetchers/fetchers.hh rename to src/libfetchers/include/nix/fetchers/fetchers.hh index b28ec456864..1f8f6bdacd6 100644 --- a/src/libfetchers/fetchers.hh +++ b/src/libfetchers/include/nix/fetchers/fetchers.hh @@ -1,17 +1,17 @@ #pragma once ///@file -#include "types.hh" -#include "hash.hh" -#include "canon-path.hh" -#include "json-impls.hh" -#include "attrs.hh" -#include "url.hh" +#include "nix/util/types.hh" +#include "nix/util/hash.hh" +#include "nix/util/canon-path.hh" +#include "nix/util/json-impls.hh" +#include "nix/fetchers/attrs.hh" +#include "nix/util/url.hh" #include #include -#include "ref.hh" +#include "nix/util/ref.hh" namespace nix { class Store; class StorePath; struct SourceAccessor; } @@ -42,9 +42,9 @@ struct Input Attrs attrs; /** - * path of the parent of this input, used for relative path resolution + * Cached result of getFingerprint(). */ - std::optional parent; + mutable std::optional> cachedFingerprint; public: /** @@ -71,7 +71,7 @@ public: ParsedURL toURL() const; - std::string toURLString(const std::map & extraQuery = {}) const; + std::string toURLString(const StringMap & extraQuery = {}) const; std::string to_string() const; @@ -90,6 +90,12 @@ public: */ bool isLocked() const; + /** + * Only for relative path flakes, i.e. 'path:./foo', returns the + * relative path, i.e. './foo'. + */ + std::optional isRelative() const; + /** * Return whether this is a "final" input, meaning that fetching * it will not add, remove or change any attributes. (See @@ -104,6 +110,11 @@ public: bool operator ==(const Input & other) const noexcept; + bool operator <(const Input & other) const + { + return attrs < other.attrs; + } + bool contains(const Input & other) const; /** @@ -144,7 +155,7 @@ public: void clone(const Path & destDir) const; - std::optional getSourcePath() const; + std::optional getSourcePath() const; /** * Write a file to this input, for input types that support @@ -227,7 +238,7 @@ struct InputScheme virtual void clone(const Input & input, const Path & destDir) const; - virtual std::optional getSourcePath(const Input & input) const; + virtual std::optional getSourcePath(const Input & input) const; virtual void putFile( const Input & input, @@ -250,6 +261,12 @@ struct InputScheme virtual bool isLocked(const Input & input) const { return false; } + + virtual std::optional isRelative(const Input & input) const + { return std::nullopt; } + + virtual std::optional getAccessToken(const fetchers::Settings & settings, const std::string & host, const std::string & url) const + { return {};} }; void registerInputScheme(std::shared_ptr && fetcher); diff --git a/src/libfetchers/filtering-source-accessor.hh b/src/libfetchers/include/nix/fetchers/filtering-source-accessor.hh similarity index 95% rename from src/libfetchers/filtering-source-accessor.hh rename to src/libfetchers/include/nix/fetchers/filtering-source-accessor.hh index 1f8d84e531e..2b59f03ca22 100644 --- a/src/libfetchers/filtering-source-accessor.hh +++ b/src/libfetchers/include/nix/fetchers/filtering-source-accessor.hh @@ -1,6 +1,8 @@ #pragma once -#include "source-path.hh" +#include "nix/util/source-path.hh" + +#include namespace nix { @@ -70,6 +72,7 @@ struct AllowListSourceAccessor : public FilteringSourceAccessor static ref create( ref next, std::set && allowedPrefixes, + std::unordered_set && allowedPaths, MakeNotAllowedError && makeNotAllowedError); using FilteringSourceAccessor::FilteringSourceAccessor; diff --git a/src/libfetchers/include/nix/fetchers/git-lfs-fetch.hh b/src/libfetchers/include/nix/fetchers/git-lfs-fetch.hh new file mode 100644 index 00000000000..b59da391a05 --- /dev/null +++ b/src/libfetchers/include/nix/fetchers/git-lfs-fetch.hh @@ -0,0 +1,46 @@ +#pragma once +///@file + +#include "nix/util/canon-path.hh" +#include "nix/util/serialise.hh" +#include "nix/util/url.hh" + +#include + +#include + +namespace nix::lfs { + +/** + * git-lfs pointer + * @see https://github.com/git-lfs/git-lfs/blob/2ef4108/docs/spec.md + */ +struct Pointer +{ + std::string oid; // git-lfs managed object id. you give this to the lfs server + // for downloads + size_t size; // in bytes +}; + +struct Fetch +{ + // Reference to the repository + const git_repository * repo; + + // Git commit being fetched + git_oid rev; + + // derived from git remote url + nix::ParsedURL url; + + Fetch(git_repository * repo, git_oid rev); + bool shouldFetch(const CanonPath & path) const; + void fetch( + const std::string & content, + const CanonPath & pointerFilePath, + StringSink & sink, + std::function sizeCallback) const; + std::vector fetchUrls(const std::vector & pointers) const; +}; + +} // namespace nix::lfs diff --git a/src/libfetchers/git-utils.hh b/src/libfetchers/include/nix/fetchers/git-utils.hh similarity index 73% rename from src/libfetchers/git-utils.hh rename to src/libfetchers/include/nix/fetchers/git-utils.hh index f45b5a50425..2926deb4f44 100644 --- a/src/libfetchers/git-utils.hh +++ b/src/libfetchers/include/nix/fetchers/git-utils.hh @@ -1,11 +1,11 @@ #pragma once -#include "filtering-source-accessor.hh" -#include "fs-sink.hh" +#include "nix/fetchers/filtering-source-accessor.hh" +#include "nix/util/fs-sink.hh" namespace nix { -namespace fetchers { struct PublicKey; } +namespace fetchers { struct PublicKey; struct Settings; } /** * A sink that writes into a Git repository. Note that nothing may be written @@ -59,12 +59,20 @@ struct GitRepo modified or added, but excluding deleted files. */ std::set files; + /* All modified or added files. */ + std::set dirtyFiles; + + /* The deleted files. */ + std::set deletedFiles; + /* The submodules listed in .gitmodules of this workdir. */ std::vector submodules; }; virtual WorkdirInfo getWorkdirInfo() = 0; + static WorkdirInfo getCachedWorkdirInfo(const std::filesystem::path & path); + /* Get the ref that HEAD points to. */ virtual std::optional getWorkdirRef() = 0; @@ -78,7 +86,11 @@ struct GitRepo virtual bool hasObject(const Hash & oid) = 0; - virtual ref getAccessor(const Hash & rev, bool exportIgnore) = 0; + virtual ref getAccessor( + const Hash & rev, + bool exportIgnore, + std::string displayPrefix, + bool smudgeLfs = false) = 0; virtual ref getAccessor(const WorkdirInfo & wd, bool exportIgnore, MakeNotAllowedError makeNotAllowedError) = 0; @@ -103,7 +115,7 @@ struct GitRepo * Given a Git tree hash, compute the hash of its NAR * serialisation. This is memoised on-disk. */ - virtual Hash treeHashToNarHash(const Hash & treeHash) = 0; + virtual Hash treeHashToNarHash(const fetchers::Settings & settings, const Hash & treeHash) = 0; /** * If the specified Git object is a directory with a single entry @@ -115,4 +127,26 @@ struct GitRepo ref getTarballCache(); +// A helper to ensure that the `git_*_free` functions get called. +template +struct Deleter +{ + template + void operator()(T * p) const { del(p); }; +}; + +// A helper to ensure that we don't leak objects returned by libgit2. +template +struct Setter +{ + T & t; + typename T::pointer p = nullptr; + + Setter(T & t) : t(t) { } + + ~Setter() { if (p) t = T(p); } + + operator typename T::pointer * () { return &p; } +}; + } diff --git a/src/libfetchers/include/nix/fetchers/input-cache.hh b/src/libfetchers/include/nix/fetchers/input-cache.hh new file mode 100644 index 00000000000..f9278053a99 --- /dev/null +++ b/src/libfetchers/include/nix/fetchers/input-cache.hh @@ -0,0 +1,35 @@ +#include "nix/fetchers/fetchers.hh" + +namespace nix::fetchers { + +enum class UseRegistries : int; + +struct InputCache +{ + struct CachedResult + { + ref accessor; + Input resolvedInput; + Input lockedInput; + }; + + CachedResult getAccessor(ref store, const Input & originalInput, UseRegistries useRegistries); + + struct CachedInput + { + Input lockedInput; + ref accessor; + }; + + virtual std::optional lookup(const Input & originalInput) const = 0; + + virtual void upsert(Input key, CachedInput cachedInput) = 0; + + virtual void clear() = 0; + + static ref create(); + + virtual ~InputCache() = default; +}; + +} diff --git a/src/libfetchers/include/nix/fetchers/meson.build b/src/libfetchers/include/nix/fetchers/meson.build new file mode 100644 index 00000000000..e6ddedd97c4 --- /dev/null +++ b/src/libfetchers/include/nix/fetchers/meson.build @@ -0,0 +1,16 @@ +include_dirs = [include_directories('../..')] + +headers = files( + 'attrs.hh', + 'cache.hh', + 'fetch-settings.hh', + 'fetch-to-store.hh', + 'fetchers.hh', + 'filtering-source-accessor.hh', + 'git-lfs-fetch.hh', + 'git-utils.hh', + 'input-cache.hh', + 'registry.hh', + 'store-path-accessor.hh', + 'tarball.hh', +) diff --git a/src/libfetchers/registry.hh b/src/libfetchers/include/nix/fetchers/registry.hh similarity index 78% rename from src/libfetchers/registry.hh rename to src/libfetchers/include/nix/fetchers/registry.hh index 0d68ac395e9..efbfe07c849 100644 --- a/src/libfetchers/registry.hh +++ b/src/libfetchers/include/nix/fetchers/registry.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "types.hh" -#include "fetchers.hh" +#include "nix/util/types.hh" +#include "nix/fetchers/fetchers.hh" namespace nix { class Store; } @@ -65,8 +65,19 @@ void overrideRegistry( const Input & to, const Attrs & extraAttrs); +enum class UseRegistries : int { + No, + All, + Limited, // global and flag registry only +}; + +/** + * Rewrite a flakeref using the registries. If `filter` is set, only + * use the registries for which the filter function returns true. + */ std::pair lookupInRegistries( ref store, - const Input & input); + const Input & input, + UseRegistries useRegistries); } diff --git a/src/libfetchers/store-path-accessor.hh b/src/libfetchers/include/nix/fetchers/store-path-accessor.hh similarity index 85% rename from src/libfetchers/store-path-accessor.hh rename to src/libfetchers/include/nix/fetchers/store-path-accessor.hh index 989cf3fa29c..021df5a628f 100644 --- a/src/libfetchers/store-path-accessor.hh +++ b/src/libfetchers/include/nix/fetchers/store-path-accessor.hh @@ -1,6 +1,6 @@ #pragma once -#include "source-path.hh" +#include "nix/util/source-path.hh" namespace nix { diff --git a/src/libfetchers/tarball.hh b/src/libfetchers/include/nix/fetchers/tarball.hh similarity index 85% rename from src/libfetchers/tarball.hh rename to src/libfetchers/include/nix/fetchers/tarball.hh index 2042041d5ad..2c5ea209f01 100644 --- a/src/libfetchers/tarball.hh +++ b/src/libfetchers/include/nix/fetchers/tarball.hh @@ -2,10 +2,10 @@ #include -#include "hash.hh" -#include "path.hh" -#include "ref.hh" -#include "types.hh" +#include "nix/util/hash.hh" +#include "nix/store/path.hh" +#include "nix/util/ref.hh" +#include "nix/util/types.hh" namespace nix { class Store; @@ -26,6 +26,7 @@ struct DownloadFileResult DownloadFileResult downloadFile( ref store, + const Settings & settings, const std::string & url, const std::string & name, const Headers & headers = {}); diff --git a/src/libfetchers/indirect.cc b/src/libfetchers/indirect.cc index 2e5cd82c78d..47cb7587cf7 100644 --- a/src/libfetchers/indirect.cc +++ b/src/libfetchers/indirect.cc @@ -1,6 +1,6 @@ -#include "fetchers.hh" -#include "url-parts.hh" -#include "path.hh" +#include "nix/fetchers/fetchers.hh" +#include "nix/util/url-parts.hh" +#include "nix/store/path.hh" namespace nix::fetchers { @@ -26,16 +26,16 @@ struct IndirectInputScheme : InputScheme else if (std::regex_match(path[1], refRegex)) ref = path[1]; else - throw BadURL("in flake URL '%s', '%s' is not a commit hash or branch/tag name", url.url, path[1]); + throw BadURL("in flake URL '%s', '%s' is not a commit hash or branch/tag name", url, path[1]); } else if (path.size() == 3) { if (!std::regex_match(path[1], refRegex)) - throw BadURL("in flake URL '%s', '%s' is not a branch/tag name", url.url, path[1]); + throw BadURL("in flake URL '%s', '%s' is not a branch/tag name", url, path[1]); ref = path[1]; if (!std::regex_match(path[2], revRegex)) - throw BadURL("in flake URL '%s', '%s' is not a commit hash", url.url, path[2]); + throw BadURL("in flake URL '%s', '%s' is not a commit hash", url, path[2]); rev = Hash::parseAny(path[2], HashAlgorithm::SHA1); } else - throw BadURL("GitHub URL '%s' is invalid", url.url); + throw BadURL("GitHub URL '%s' is invalid", url); std::string id = path[0]; if (!std::regex_match(id, flakeRegex)) diff --git a/src/libfetchers/input-cache.cc b/src/libfetchers/input-cache.cc new file mode 100644 index 00000000000..1a4bb28a326 --- /dev/null +++ b/src/libfetchers/input-cache.cc @@ -0,0 +1,76 @@ +#include "nix/fetchers/input-cache.hh" +#include "nix/fetchers/registry.hh" +#include "nix/util/sync.hh" +#include "nix/util/source-path.hh" + +namespace nix::fetchers { + +InputCache::CachedResult +InputCache::getAccessor(ref store, const Input & originalInput, UseRegistries useRegistries) +{ + auto fetched = lookup(originalInput); + Input resolvedInput = originalInput; + + if (!fetched) { + if (originalInput.isDirect()) { + auto [accessor, lockedInput] = originalInput.getAccessor(store); + fetched.emplace(CachedInput{.lockedInput = lockedInput, .accessor = accessor}); + } else { + if (useRegistries != UseRegistries::No) { + auto [res, extraAttrs] = lookupInRegistries(store, originalInput, useRegistries); + resolvedInput = std::move(res); + fetched = lookup(resolvedInput); + if (!fetched) { + auto [accessor, lockedInput] = resolvedInput.getAccessor(store); + fetched.emplace(CachedInput{.lockedInput = lockedInput, .accessor = accessor}); + } + upsert(resolvedInput, *fetched); + } else { + throw Error( + "'%s' is an indirect flake reference, but registry lookups are not allowed", + originalInput.to_string()); + } + } + upsert(originalInput, *fetched); + } + + debug("got tree '%s' from '%s'", fetched->accessor, fetched->lockedInput.to_string()); + + return {fetched->accessor, resolvedInput, fetched->lockedInput}; +} + +struct InputCacheImpl : InputCache +{ + Sync> cache_; + + std::optional lookup(const Input & originalInput) const override + { + auto cache(cache_.readLock()); + auto i = cache->find(originalInput); + if (i == cache->end()) + return std::nullopt; + debug( + "mapping '%s' to previously seen input '%s' -> '%s", + originalInput.to_string(), + i->first.to_string(), + i->second.lockedInput.to_string()); + return i->second; + } + + void upsert(Input key, CachedInput cachedInput) override + { + cache_.lock()->insert_or_assign(std::move(key), std::move(cachedInput)); + } + + void clear() override + { + cache_.lock()->clear(); + } +}; + +ref InputCache::create() +{ + return make_ref(); +} + +} diff --git a/src/libfetchers/mercurial.cc b/src/libfetchers/mercurial.cc index 2c987f79d50..0b63876deae 100644 --- a/src/libfetchers/mercurial.cc +++ b/src/libfetchers/mercurial.cc @@ -1,13 +1,13 @@ -#include "fetchers.hh" -#include "processes.hh" -#include "users.hh" -#include "cache.hh" -#include "globals.hh" -#include "tarfile.hh" -#include "store-api.hh" -#include "url-parts.hh" -#include "store-path-accessor.hh" -#include "fetch-settings.hh" +#include "nix/fetchers/fetchers.hh" +#include "nix/util/processes.hh" +#include "nix/util/users.hh" +#include "nix/fetchers/cache.hh" +#include "nix/store/globals.hh" +#include "nix/util/tarfile.hh" +#include "nix/store/store-api.hh" +#include "nix/util/url-parts.hh" +#include "nix/fetchers/store-path-accessor.hh" +#include "nix/fetchers/fetch-settings.hh" #include @@ -126,7 +126,7 @@ struct MercurialInputScheme : InputScheme return res; } - std::optional getSourcePath(const Input & input) const override + std::optional getSourcePath(const Input & input) const override { auto url = parseURL(getStrAttr(input.attrs, "url")); if (url.scheme == "file" && !input.getRef() && !input.getRev()) @@ -161,7 +161,7 @@ struct MercurialInputScheme : InputScheme { auto url = parseURL(getStrAttr(input.attrs, "url")); bool isLocal = url.scheme == "file"; - return {isLocal, isLocal ? url.path : url.base}; + return {isLocal, isLocal ? url.path : url.to_string()}; } StorePath fetchToStore(ref store, Input & input) const @@ -194,7 +194,7 @@ struct MercurialInputScheme : InputScheme input.attrs.insert_or_assign("ref", chomp(runHg({ "branch", "-R", actualUrl }))); - auto files = tokenizeString>( + auto files = tokenizeString( runHg({ "status", "-R", actualUrl, "--clean", "--modified", "--added", "--no-status", "--print0" }), "\0"s); Path actualPath(absPath(actualUrl)); @@ -253,13 +253,13 @@ struct MercurialInputScheme : InputScheme }}; if (!input.getRev()) { - if (auto res = getCache()->lookupWithTTL(refToRevKey)) + if (auto res = input.settings->getCache()->lookupWithTTL(refToRevKey)) input.attrs.insert_or_assign("rev", getRevAttr(*res, "rev").gitRev()); } /* If we have a rev, check if we have a cached store path. */ if (auto rev = input.getRev()) { - if (auto res = getCache()->lookupStorePath(revInfoKey(*rev), *store)) + if (auto res = input.settings->getCache()->lookupStorePath(revInfoKey(*rev), *store)) return makeResult(res->value, res->storePath); } @@ -309,7 +309,7 @@ struct MercurialInputScheme : InputScheme /* Now that we have the rev, check the cache again for a cached store path. */ - if (auto res = getCache()->lookupStorePath(revInfoKey(rev), *store)) + if (auto res = input.settings->getCache()->lookupStorePath(revInfoKey(rev), *store)) return makeResult(res->value, res->storePath); Path tmpDir = createTempDir(); @@ -326,9 +326,9 @@ struct MercurialInputScheme : InputScheme }); if (!origRev) - getCache()->upsert(refToRevKey, {{"rev", rev.gitRev()}}); + input.settings->getCache()->upsert(refToRevKey, {{"rev", rev.gitRev()}}); - getCache()->upsert(revInfoKey(rev), *store, infoAttrs, storePath); + input.settings->getCache()->upsert(revInfoKey(rev), *store, infoAttrs, storePath); return makeResult(infoAttrs, std::move(storePath)); } diff --git a/src/libfetchers/meson.build b/src/libfetchers/meson.build index 07a1178cced..321146ca4ed 100644 --- a/src/libfetchers/meson.build +++ b/src/libfetchers/meson.build @@ -4,8 +4,6 @@ project('nix-fetchers', 'cpp', 'cpp_std=c++2a', # TODO(Qyriad): increase the warning level 'warning_level=1', - 'debug=true', - 'optimization=2', 'errorlogs=true', # Please print logs for tests that fail ], meson_version : '>= 1.1', @@ -14,9 +12,9 @@ project('nix-fetchers', 'cpp', cxx = meson.get_compiler('cpp') -subdir('build-utils-meson/deps-lists') +subdir('nix-meson-build-support/deps-lists') -configdata = configuration_data() +configuration_data() deps_private_maybe_subproject = [ ] @@ -24,7 +22,7 @@ deps_public_maybe_subproject = [ dependency('nix-util'), dependency('nix-store'), ] -subdir('build-utils-meson/subprojects') +subdir('nix-meson-build-support/subprojects') nlohmann_json = dependency('nlohmann_json', version : '>= 3.9') deps_public += nlohmann_json @@ -32,16 +30,7 @@ deps_public += nlohmann_json libgit2 = dependency('libgit2') deps_private += libgit2 -add_project_arguments( - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. - '-include', 'config-util.hh', - '-include', 'config-store.hh', - # '-include', 'config-fetchers.h', - language : 'cpp', -) - -subdir('build-utils-meson/common') +subdir('nix-meson-build-support/common') sources = files( 'attrs.cc', @@ -50,44 +39,36 @@ sources = files( 'fetch-to-store.cc', 'fetchers.cc', 'filtering-source-accessor.cc', + 'git-lfs-fetch.cc', 'git-utils.cc', 'git.cc', 'github.cc', 'indirect.cc', + 'input-cache.cc', 'mercurial.cc', - 'mounted-source-accessor.cc', 'path.cc', 'registry.cc', 'store-path-accessor.cc', 'tarball.cc', ) -include_dirs = [include_directories('.')] +subdir('include/nix/fetchers') -headers = files( - 'attrs.hh', - 'cache.hh', - 'fetch-settings.hh', - 'fetch-to-store.hh', - 'fetchers.hh', - 'filtering-source-accessor.hh', - 'git-utils.hh', - 'mounted-source-accessor.hh', - 'registry.hh', - 'store-path-accessor.hh', - 'tarball.hh', -) +subdir('nix-meson-build-support/export-all-symbols') +subdir('nix-meson-build-support/windows-version') this_library = library( 'nixfetchers', sources, dependencies : deps_public + deps_private + deps_other, + include_directories : include_dirs, + link_args: linker_export_flags, prelink : true, # For C++ static initializers install : true, ) -install_headers(headers, subdir : 'nix', preserve_path : true) +install_headers(headers, subdir : 'nix/fetchers', preserve_path : true) libraries_private = [] -subdir('build-utils-meson/export') +subdir('nix-meson-build-support/export') diff --git a/src/libfetchers/mounted-source-accessor.hh b/src/libfetchers/mounted-source-accessor.hh deleted file mode 100644 index 45cbcb09a24..00000000000 --- a/src/libfetchers/mounted-source-accessor.hh +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include "source-accessor.hh" - -namespace nix { - -ref makeMountedSourceAccessor(std::map> mounts); - -} diff --git a/src/libfetchers/nix-meson-build-support b/src/libfetchers/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/libfetchers/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/libfetchers/package.nix b/src/libfetchers/package.nix index 70973bdb26c..14592087999 100644 --- a/src/libfetchers/package.nix +++ b/src/libfetchers/package.nix @@ -1,15 +1,15 @@ -{ lib -, stdenv -, mkMesonLibrary +{ + lib, + mkMesonLibrary, -, nix-util -, nix-store -, nlohmann_json -, libgit2 + nix-util, + nix-store, + nlohmann_json, + libgit2, -# Configuration Options + # Configuration Options -, version + version, }: let @@ -22,11 +22,12 @@ mkMesonLibrary (finalAttrs: { workDir = ./.; fileset = fileset.unions [ - ../../build-utils-meson - ./build-utils-meson + ../../nix-meson-build-support + ./nix-meson-build-support ../../.version ./.version ./meson.build + ./include/nix/fetchers/meson.build (fileset.fileFilter (file: file.hasExt "cc") ./.) (fileset.fileFilter (file: file.hasExt "hh") ./.) ]; @@ -41,18 +42,6 @@ mkMesonLibrary (finalAttrs: { nlohmann_json ]; - preConfigure = - # "Inline" .version so it's not a symlink, and includes the suffix. - # Do the meson utils, without modification. - '' - chmod u+w ./.version - echo ${version} > ../../.version - ''; - - env = lib.optionalAttrs (stdenv.isLinux && !(stdenv.hostPlatform.isStatic && stdenv.system == "aarch64-linux")) { - LDFLAGS = "-fuse-ld=gold"; - }; - meta = { platforms = lib.platforms.unix ++ lib.platforms.windows; }; diff --git a/src/libfetchers/path.cc b/src/libfetchers/path.cc index 246b68c3a9b..9239fd27466 100644 --- a/src/libfetchers/path.cc +++ b/src/libfetchers/path.cc @@ -1,7 +1,10 @@ -#include "fetchers.hh" -#include "store-api.hh" -#include "archive.hh" -#include "store-path-accessor.hh" +#include "nix/fetchers/fetchers.hh" +#include "nix/store/store-api.hh" +#include "nix/util/archive.hh" +#include "nix/fetchers/store-path-accessor.hh" +#include "nix/fetchers/cache.hh" +#include "nix/fetchers/fetch-to-store.hh" +#include "nix/fetchers/fetch-settings.hh" namespace nix::fetchers { @@ -14,7 +17,7 @@ struct PathInputScheme : InputScheme if (url.scheme != "path") return {}; if (url.authority && *url.authority != "") - throw Error("path URL '%s' should not have an authority ('%s')", url.url, *url.authority); + throw Error("path URL '%s' should not have an authority ('%s')", url, *url.authority); Input input{settings}; input.attrs.insert_or_assign("type", "path"); @@ -27,10 +30,10 @@ struct PathInputScheme : InputScheme if (auto n = string2Int(value)) input.attrs.insert_or_assign(name, *n); else - throw Error("path URL '%s' has invalid parameter '%s'", url.to_string(), name); + throw Error("path URL '%s' has invalid parameter '%s'", url, name); } else - throw Error("path URL '%s' has unsupported parameter '%s'", url.to_string(), name); + throw Error("path URL '%s' has unsupported parameter '%s'", url, name); return input; } @@ -80,9 +83,9 @@ struct PathInputScheme : InputScheme }; } - std::optional getSourcePath(const Input & input) const override + std::optional getSourcePath(const Input & input) const override { - return getStrAttr(input.attrs, "path"); + return getAbsPath(input); } void putFile( @@ -91,13 +94,13 @@ struct PathInputScheme : InputScheme std::string_view contents, std::optional commitMsg) const override { - writeFile((CanonPath(getAbsPath(input)) / path).abs(), contents); + writeFile(getAbsPath(input) / path.rel(), contents); } - std::optional isRelative(const Input & input) const + std::optional isRelative(const Input & input) const override { auto path = getStrAttr(input.attrs, "path"); - if (hasPrefix(path, "/")) + if (isAbsolute(path)) return std::nullopt; else return path; @@ -108,12 +111,12 @@ struct PathInputScheme : InputScheme return (bool) input.getNarHash(); } - CanonPath getAbsPath(const Input & input) const + std::filesystem::path getAbsPath(const Input & input) const { auto path = getStrAttr(input.attrs, "path"); - if (path[0] == '/') - return CanonPath(path); + if (isAbsolute(path)) + return canonPath(path); throw Error("cannot fetch input '%s' because it uses a relative path", input.to_string()); } @@ -121,31 +124,14 @@ struct PathInputScheme : InputScheme std::pair, Input> getAccessor(ref store, const Input & _input) const override { Input input(_input); - std::string absPath; auto path = getStrAttr(input.attrs, "path"); - if (path[0] != '/') { - if (!input.parent) - throw Error("cannot fetch input '%s' because it uses a relative path", input.to_string()); + auto absPath = getAbsPath(input); - auto parent = canonPath(*input.parent); - - // the path isn't relative, prefix it - absPath = nix::absPath(path, parent); - - // for security, ensure that if the parent is a store path, it's inside it - if (store->isInStore(parent)) { - auto storePath = store->printStorePath(store->toStorePath(parent).first); - if (!isDirOrInDir(absPath, storePath)) - throw BadStorePath("relative path '%s' points outside of its parent's store path '%s'", path, storePath); - } - } else - absPath = path; - - Activity act(*logger, lvlTalkative, actUnknown, fmt("copying '%s'", absPath)); + Activity act(*logger, lvlTalkative, actUnknown, fmt("copying %s to the store", absPath)); // FIXME: check whether access to 'path' is allowed. - auto storePath = store->maybeParseStorePath(absPath); + auto storePath = store->maybeParseStorePath(absPath.string()); if (storePath) store->addTempRoot(*storePath); @@ -154,11 +140,19 @@ struct PathInputScheme : InputScheme if (!storePath || storePath->name() != "source" || !store->isValidPath(*storePath)) { // FIXME: try to substitute storePath. auto src = sinkToSource([&](Sink & sink) { - mtime = dumpPathAndGetMtime(absPath, sink, defaultPathFilter); + mtime = dumpPathAndGetMtime(absPath.string(), sink, defaultPathFilter); }); storePath = store->addToStoreFromDump(*src, "source"); } + // To avoid copying the path again to the /nix/store, we need to add a cache entry. + ContentAddressMethod method = ContentAddressMethod::Raw::NixArchive; + auto fp = getFingerprint(store, input); + if (fp) { + auto cacheKey = makeFetchToStoreCacheKey(input.getName(), *fp, method, "/"); + input.settings->getCache()->upsert(cacheKey, *store, {}, *storePath); + } + /* Trust the lastModified value supplied by the user, if any. It's not a "secure" attribute so we don't care. */ if (!input.getLastModified()) @@ -176,7 +170,7 @@ struct PathInputScheme : InputScheme store object and the subpath. */ auto path = getAbsPath(input); try { - auto [storePath, subPath] = store->toStorePath(path.abs()); + auto [storePath, subPath] = store->toStorePath(path.string()); auto info = store->queryPathInfo(storePath); return fmt("path:%s:%s", info->narHash.to_string(HashFormat::Base16, false), subPath); } catch (Error &) { diff --git a/src/libfetchers/registry.cc b/src/libfetchers/registry.cc index c761028ab55..335935f53af 100644 --- a/src/libfetchers/registry.cc +++ b/src/libfetchers/registry.cc @@ -1,10 +1,10 @@ -#include "fetch-settings.hh" -#include "registry.hh" -#include "tarball.hh" -#include "users.hh" -#include "globals.hh" -#include "store-api.hh" -#include "local-fs-store.hh" +#include "nix/fetchers/fetch-settings.hh" +#include "nix/fetchers/registry.hh" +#include "nix/fetchers/tarball.hh" +#include "nix/util/users.hh" +#include "nix/store/globals.hh" +#include "nix/store/store-api.hh" +#include "nix/store/local-fs-store.hh" #include @@ -14,6 +14,8 @@ std::shared_ptr Registry::read( const Settings & settings, const Path & path, RegistryType type) { + debug("reading registry '%s'", path); + auto registry = std::make_shared(settings, type); if (!pathExists(path)) @@ -153,8 +155,8 @@ static std::shared_ptr getGlobalRegistry(const Settings & settings, re return std::make_shared(settings, Registry::Global); // empty registry } - if (!hasPrefix(path, "/")) { - auto storePath = downloadFile(store, path, "flake-registry.json").storePath; + if (!isAbsolute(path)) { + auto storePath = downloadFile(store, settings, path, "flake-registry.json").storePath; if (auto store2 = store.dynamic_pointer_cast()) store2->addPermRoot(storePath, getCacheDir() + "/flake-registry.json"); path = store->toRealPath(storePath); @@ -178,28 +180,37 @@ Registries getRegistries(const Settings & settings, ref store) std::pair lookupInRegistries( ref store, - const Input & _input) + const Input & _input, + UseRegistries useRegistries) { Attrs extraAttrs; int n = 0; Input input(_input); + if (useRegistries == UseRegistries::No) + return {input, extraAttrs}; + restart: n++; if (n > 100) throw Error("cycle detected in flake registry for '%s'", input.to_string()); for (auto & registry : getRegistries(*input.settings, store)) { + if (useRegistries == UseRegistries::Limited + && !(registry->type == fetchers::Registry::Flag || registry->type == fetchers::Registry::Global)) + continue; // FIXME: O(n) for (auto & entry : registry->entries) { if (entry.exact) { if (entry.from == input) { + debug("resolved flakeref '%s' against registry %d exactly", input.to_string(), registry->type); input = entry.to; extraAttrs = entry.extraAttrs; goto restart; } } else { if (entry.from.contains(input)) { + debug("resolved flakeref '%s' against registry %d", input.to_string(), registry->type); input = entry.to.applyOverrides( !entry.from.getRef() && input.getRef() ? input.getRef() : std::optional(), !entry.from.getRev() && input.getRev() ? input.getRev() : std::optional()); diff --git a/src/libfetchers/store-path-accessor.cc b/src/libfetchers/store-path-accessor.cc index 528bf2a4f51..f389d03276a 100644 --- a/src/libfetchers/store-path-accessor.cc +++ b/src/libfetchers/store-path-accessor.cc @@ -1,15 +1,11 @@ -#include "store-path-accessor.hh" -#include "store-api.hh" +#include "nix/fetchers/store-path-accessor.hh" +#include "nix/store/store-api.hh" namespace nix { ref makeStorePathAccessor(ref store, const StorePath & storePath) { - // FIXME: should use `store->getFSAccessor()` - auto root = std::filesystem::path{store->toRealPath(storePath)}; - auto accessor = makeFSSourceAccessor(root); - accessor->setPathDisplay(root.string()); - return accessor; + return projectSubdirSourceAccessor(store->getFSAccessor(), storePath.to_string()); } } diff --git a/src/libfetchers/tarball.cc b/src/libfetchers/tarball.cc index 28574e7b1e7..b0822cc3301 100644 --- a/src/libfetchers/tarball.cc +++ b/src/libfetchers/tarball.cc @@ -1,19 +1,21 @@ -#include "tarball.hh" -#include "fetchers.hh" -#include "cache.hh" -#include "filetransfer.hh" -#include "store-api.hh" -#include "archive.hh" -#include "tarfile.hh" -#include "types.hh" -#include "store-path-accessor.hh" -#include "store-api.hh" -#include "git-utils.hh" +#include "nix/fetchers/tarball.hh" +#include "nix/fetchers/fetchers.hh" +#include "nix/fetchers/cache.hh" +#include "nix/store/filetransfer.hh" +#include "nix/store/store-api.hh" +#include "nix/util/archive.hh" +#include "nix/util/tarfile.hh" +#include "nix/util/types.hh" +#include "nix/fetchers/store-path-accessor.hh" +#include "nix/store/store-api.hh" +#include "nix/fetchers/git-utils.hh" +#include "nix/fetchers/fetch-settings.hh" namespace nix::fetchers { DownloadFileResult downloadFile( ref store, + const Settings & settings, const std::string & url, const std::string & name, const Headers & headers) @@ -25,7 +27,7 @@ DownloadFileResult downloadFile( {"name", name}, }}}; - auto cached = getCache()->lookupStorePath(key, *store); + auto cached = settings.getCache()->lookupStorePath(key, *store); auto useCached = [&]() -> DownloadFileResult { @@ -92,7 +94,7 @@ DownloadFileResult downloadFile( key.second.insert_or_assign("url", url); assert(!res.urls.empty()); infoAttrs.insert_or_assign("url", *res.urls.rbegin()); - getCache()->upsert(key, *store, infoAttrs, *storePath); + settings.getCache()->upsert(key, *store, infoAttrs, *storePath); } return { @@ -104,12 +106,14 @@ DownloadFileResult downloadFile( } static DownloadTarballResult downloadTarball_( + const Settings & settings, const std::string & url, - const Headers & headers) + const Headers & headers, + const std::string & displayPrefix) { Cache::Key cacheKey{"tarball", {{"url", url}}}; - auto cached = getCache()->lookupExpired(cacheKey); + auto cached = settings.getCache()->lookupExpired(cacheKey); auto attrsToResult = [&](const Attrs & infoAttrs) { @@ -118,7 +122,7 @@ static DownloadTarballResult downloadTarball_( .treeHash = treeHash, .lastModified = (time_t) getIntAttr(infoAttrs, "lastModified"), .immutableUrl = maybeGetStrAttr(infoAttrs, "immutableUrl"), - .accessor = getTarballCache()->getAccessor(treeHash, false), + .accessor = getTarballCache()->getAccessor(treeHash, false, displayPrefix), }; }; @@ -195,7 +199,7 @@ static DownloadTarballResult downloadTarball_( /* Insert a cache entry for every URL in the redirect chain. */ for (auto & url : res->urls) { cacheKey.second.insert_or_assign("url", url); - getCache()->upsert(cacheKey, infoAttrs); + settings.getCache()->upsert(cacheKey, infoAttrs); } // FIXME: add a cache entry for immutableUrl? That could allow @@ -223,7 +227,7 @@ ref downloadTarball( // An input scheme corresponding to a curl-downloadable resource. struct CurlInputScheme : InputScheme { - const std::set transportUrlSchemes = {"file", "http", "https"}; + const StringSet transportUrlSchemes = {"file", "http", "https"}; bool hasTarballExtension(std::string_view path) const { @@ -235,7 +239,7 @@ struct CurlInputScheme : InputScheme virtual bool isValidURL(const ParsedURL & url, bool requireTree) const = 0; - static const std::set specialParams; + static const StringSet specialParams; std::optional inputFromURL( const Settings & settings, @@ -340,7 +344,7 @@ struct FileInputScheme : CurlInputScheme the Nix store directly, since there is little deduplication benefit in using the Git cache for single big files like tarballs. */ - auto file = downloadFile(store, getStrAttr(input.attrs, "url"), input.getName()); + auto file = downloadFile(store, *input.settings, getStrAttr(input.attrs, "url"), input.getName()); auto narHash = store->queryPathInfo(file.storePath)->narHash; input.attrs.insert_or_assign("narHash", narHash.to_string(HashFormat::SRI, true)); @@ -371,9 +375,11 @@ struct TarballInputScheme : CurlInputScheme { auto input(_input); - auto result = downloadTarball_(getStrAttr(input.attrs, "url"), {}); - - result.accessor->setPathDisplay("«" + input.to_string() + "»"); + auto result = downloadTarball_( + *input.settings, + getStrAttr(input.attrs, "url"), + {}, + "«" + input.to_string() + "»"); if (result.immutableUrl) { auto immutableInput = Input::fromURL(*input.settings, *result.immutableUrl); @@ -388,7 +394,7 @@ struct TarballInputScheme : CurlInputScheme input.attrs.insert_or_assign("lastModified", uint64_t(result.lastModified)); input.attrs.insert_or_assign("narHash", - getTarballCache()->treeHashToNarHash(result.treeHash).to_string(HashFormat::SRI, true)); + getTarballCache()->treeHashToNarHash(*input.settings, result.treeHash).to_string(HashFormat::SRI, true)); return {result.accessor, input}; } diff --git a/src/libflake-c/build-utils-meson b/src/libflake-c/build-utils-meson deleted file mode 120000 index 91937f18372..00000000000 --- a/src/libflake-c/build-utils-meson +++ /dev/null @@ -1 +0,0 @@ -../../build-utils-meson/ \ No newline at end of file diff --git a/src/libflake-c/meson.build b/src/libflake-c/meson.build index 00d9650e770..5a81618c809 100644 --- a/src/libflake-c/meson.build +++ b/src/libflake-c/meson.build @@ -4,8 +4,6 @@ project('nix-flake-c', 'cpp', 'cpp_std=c++2a', # TODO(Qyriad): increase the warning level 'warning_level=1', - 'debug=true', - 'optimization=2', 'errorlogs=true', # Please print logs for tests that fail ], meson_version : '>= 1.1', @@ -14,51 +12,24 @@ project('nix-flake-c', 'cpp', cxx = meson.get_compiler('cpp') -subdir('build-utils-meson/deps-lists') - -configdata = configuration_data() +subdir('nix-meson-build-support/deps-lists') deps_private_maybe_subproject = [ dependency('nix-util'), dependency('nix-store'), + dependency('nix-fetchers'), dependency('nix-expr'), dependency('nix-flake'), ] deps_public_maybe_subproject = [ dependency('nix-util-c'), dependency('nix-store-c'), + dependency('nix-fetchers-c'), dependency('nix-expr-c'), ] -subdir('build-utils-meson/subprojects') - -# TODO rename, because it will conflict with downstream projects -configdata.set_quoted('PACKAGE_VERSION', meson.project_version()) - -config_h = configure_file( - configuration : configdata, - output : 'config-flake.h', -) - -add_project_arguments( - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. - - # From C++ libraries, only for internals - '-include', 'config-util.hh', - '-include', 'config-store.hh', - '-include', 'config-expr.hh', - # not generated (yet?) - # '-include', 'config-flake.hh', - - # From C libraries, for our public, installed headers too - '-include', 'config-util.h', - '-include', 'config-store.h', - '-include', 'config-expr.h', - '-include', 'config-flake.h', - language : 'cpp', -) +subdir('nix-meson-build-support/subprojects') -subdir('build-utils-meson/common') +subdir('nix-meson-build-support/common') sources = files( 'nix_api_flake.cc', @@ -66,15 +37,16 @@ sources = files( include_dirs = [include_directories('.')] -headers = [config_h] + files( +headers = files( 'nix_api_flake.h', + 'nix_api_flake_internal.hh', ) # TODO move this header to libexpr, maybe don't use it in tests? headers += files('nix_api_flake.h') -subdir('build-utils-meson/export-all-symbols') -subdir('build-utils-meson/windows-version') +subdir('nix-meson-build-support/export-all-symbols') +subdir('nix-meson-build-support/windows-version') this_library = library( 'nixflakec', @@ -86,8 +58,8 @@ this_library = library( install : true, ) -install_headers(headers, subdir : 'nix', preserve_path : true) +install_headers(headers, preserve_path : true) libraries_private = [] -subdir('build-utils-meson/export') +subdir('nix-meson-build-support/export') diff --git a/src/libflake-c/nix-meson-build-support b/src/libflake-c/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/libflake-c/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/libflake-c/nix_api_flake.cc b/src/libflake-c/nix_api_flake.cc index 17cf6572da2..ad8f0bf4ec4 100644 --- a/src/libflake-c/nix_api_flake.cc +++ b/src/libflake-c/nix_api_flake.cc @@ -1,11 +1,18 @@ +#include + #include "nix_api_flake.h" #include "nix_api_flake_internal.hh" +#include "nix_api_util.h" #include "nix_api_util_internal.h" +#include "nix_api_expr_internal.h" +#include "nix_api_fetchers_internal.hh" +#include "nix_api_fetchers.h" -#include "flake/flake.hh" +#include "nix/flake/flake.hh" nix_flake_settings * nix_flake_settings_new(nix_c_context * context) { + nix_clear_err(context); try { auto settings = nix::make_ref(); return new nix_flake_settings{settings}; @@ -18,15 +25,181 @@ void nix_flake_settings_free(nix_flake_settings * settings) delete settings; } -nix_err nix_flake_init_global(nix_c_context * context, nix_flake_settings * settings) +nix_err nix_flake_settings_add_to_eval_state_builder( + nix_c_context * context, nix_flake_settings * settings, nix_eval_state_builder * builder) +{ + nix_clear_err(context); + try { + settings->settings->configureEvalSettings(builder->settings); + } + NIXC_CATCH_ERRS +} + +nix_flake_reference_parse_flags * +nix_flake_reference_parse_flags_new(nix_c_context * context, nix_flake_settings * settings) +{ + nix_clear_err(context); + try { + return new nix_flake_reference_parse_flags{ + .baseDirectory = std::nullopt, + }; + } + NIXC_CATCH_ERRS_NULL +} + +void nix_flake_reference_parse_flags_free(nix_flake_reference_parse_flags * flags) +{ + delete flags; +} + +nix_err nix_flake_reference_parse_flags_set_base_directory( + nix_c_context * context, + nix_flake_reference_parse_flags * flags, + const char * baseDirectory, + size_t baseDirectoryLen) +{ + nix_clear_err(context); + try { + flags->baseDirectory.emplace(nix::Path{std::string(baseDirectory, baseDirectoryLen)}); + return NIX_OK; + } + NIXC_CATCH_ERRS +} + +nix_err nix_flake_reference_and_fragment_from_string( + nix_c_context * context, + nix_fetchers_settings * fetchSettings, + nix_flake_settings * flakeSettings, + nix_flake_reference_parse_flags * parseFlags, + const char * strData, + size_t strSize, + nix_flake_reference ** flakeReferenceOut, + nix_get_string_callback fragmentCallback, + void * fragmentCallbackUserData) +{ + nix_clear_err(context); + *flakeReferenceOut = nullptr; + try { + std::string str(strData, strSize); + + auto [flakeRef, fragment] = + nix::parseFlakeRefWithFragment(*fetchSettings->settings, str, parseFlags->baseDirectory, true); + *flakeReferenceOut = new nix_flake_reference{nix::make_ref(flakeRef)}; + return call_nix_get_string_callback(fragment, fragmentCallback, fragmentCallbackUserData); + } + NIXC_CATCH_ERRS +} + +void nix_flake_reference_free(nix_flake_reference * flakeReference) +{ + delete flakeReference; +} + +nix_flake_lock_flags * nix_flake_lock_flags_new(nix_c_context * context, nix_flake_settings * settings) +{ + nix_clear_err(context); + try { + auto lockSettings = nix::make_ref(nix::flake::LockFlags{ + .recreateLockFile = false, + .updateLockFile = true, // == `nix_flake_lock_flags_set_mode_write_as_needed` + .writeLockFile = true, // == `nix_flake_lock_flags_set_mode_write_as_needed` + .failOnUnlocked = false, // == `nix_flake_lock_flags_set_mode_write_as_needed` + .useRegistries = false, + .allowUnlocked = false, // == `nix_flake_lock_flags_set_mode_write_as_needed` + .commitLockFile = false, + + }); + return new nix_flake_lock_flags{lockSettings}; + } + NIXC_CATCH_ERRS_NULL +} + +void nix_flake_lock_flags_free(nix_flake_lock_flags * flags) +{ + delete flags; +} + +nix_err nix_flake_lock_flags_set_mode_virtual(nix_c_context * context, nix_flake_lock_flags * flags) { - static std::shared_ptr registeredSettings; + nix_clear_err(context); try { - if (registeredSettings) - throw nix::Error("nix_flake_init_global already initialized"); + flags->lockFlags->updateLockFile = true; + flags->lockFlags->writeLockFile = false; + flags->lockFlags->failOnUnlocked = false; + flags->lockFlags->allowUnlocked = true; + } + NIXC_CATCH_ERRS +} - registeredSettings = settings->settings; - nix::flake::initLib(*registeredSettings); +nix_err nix_flake_lock_flags_set_mode_write_as_needed(nix_c_context * context, nix_flake_lock_flags * flags) +{ + nix_clear_err(context); + try { + flags->lockFlags->updateLockFile = true; + flags->lockFlags->writeLockFile = true; + flags->lockFlags->failOnUnlocked = false; + flags->lockFlags->allowUnlocked = true; } NIXC_CATCH_ERRS } + +nix_err nix_flake_lock_flags_set_mode_check(nix_c_context * context, nix_flake_lock_flags * flags) +{ + nix_clear_err(context); + try { + flags->lockFlags->updateLockFile = false; + flags->lockFlags->writeLockFile = false; + flags->lockFlags->failOnUnlocked = true; + flags->lockFlags->allowUnlocked = false; + } + NIXC_CATCH_ERRS +} + +nix_err nix_flake_lock_flags_add_input_override( + nix_c_context * context, nix_flake_lock_flags * flags, const char * inputPath, nix_flake_reference * flakeRef) +{ + nix_clear_err(context); + try { + auto path = nix::flake::parseInputAttrPath(inputPath); + flags->lockFlags->inputOverrides.emplace(path, *flakeRef->flakeRef); + if (flags->lockFlags->writeLockFile) { + return nix_flake_lock_flags_set_mode_virtual(context, flags); + } + } + NIXC_CATCH_ERRS +} + +nix_locked_flake * nix_flake_lock( + nix_c_context * context, + nix_fetchers_settings * fetchSettings, + nix_flake_settings * flakeSettings, + EvalState * eval_state, + nix_flake_lock_flags * flags, + nix_flake_reference * flakeReference) +{ + nix_clear_err(context); + try { + eval_state->state.resetFileCache(); + auto lockedFlake = nix::make_ref(nix::flake::lockFlake( + *flakeSettings->settings, eval_state->state, *flakeReference->flakeRef, *flags->lockFlags)); + return new nix_locked_flake{lockedFlake}; + } + NIXC_CATCH_ERRS_NULL +} + +void nix_locked_flake_free(nix_locked_flake * lockedFlake) +{ + delete lockedFlake; +} + +nix_value * nix_locked_flake_get_output_attrs( + nix_c_context * context, nix_flake_settings * settings, EvalState * evalState, nix_locked_flake * lockedFlake) +{ + nix_clear_err(context); + try { + auto v = nix_alloc_value(context, evalState); + nix::flake::callFlake(evalState->state, *lockedFlake->lockedFlake, v->value); + return v; + } + NIXC_CATCH_ERRS_NULL +} diff --git a/src/libflake-c/nix_api_flake.h b/src/libflake-c/nix_api_flake.h index 80051298d28..a1a7060a614 100644 --- a/src/libflake-c/nix_api_flake.h +++ b/src/libflake-c/nix_api_flake.h @@ -9,6 +9,7 @@ * @brief Main entry for the libflake C bindings */ +#include "nix_api_fetchers.h" #include "nix_api_store.h" #include "nix_api_util.h" #include "nix_api_expr.h" @@ -18,8 +19,46 @@ extern "C" { #endif // cffi start +/** + * @brief A settings object for configuring the behavior of the nix-flake-c library. + * @see nix_flake_settings_new + * @see nix_flake_settings_free + */ typedef struct nix_flake_settings nix_flake_settings; +/** + * @brief Context and parameters for parsing a flake reference + * @see nix_flake_reference_parse_flags_free + * @see nix_flake_reference_parse_string + */ +typedef struct nix_flake_reference_parse_flags nix_flake_reference_parse_flags; + +/** + * @brief A reference to a flake + * + * A flake reference specifies how to fetch a flake. + * + * @see nix_flake_reference_from_string + * @see nix_flake_reference_free + */ +typedef struct nix_flake_reference nix_flake_reference; + +/** + * @brief Parameters for locking a flake + * @see nix_flake_lock_flags_new + * @see nix_flake_lock_flags_free + * @see nix_flake_lock + */ +typedef struct nix_flake_lock_flags nix_flake_lock_flags; + +/** + * @brief A flake with a suitable lock (file or otherwise) + * @see nix_flake_lock + * @see nix_locked_flake_free + * @see nix_locked_flake_get_output_attrs + */ +typedef struct nix_locked_flake nix_locked_flake; + // Function prototypes /** * Create a nix_flake_settings initialized with default values. @@ -35,9 +74,169 @@ nix_flake_settings * nix_flake_settings_new(nix_c_context * context); void nix_flake_settings_free(nix_flake_settings * settings); /** - * @brief Register Flakes support process-wide. + * @brief Initialize a `nix_flake_settings` to contain `builtins.getFlake` and + * potentially more. + * + * @warning This does not put the eval state in pure mode! + * + * @param[out] context Optional, stores error information + * @param[in] settings The settings to use for e.g. `builtins.getFlake` + * @param[in] builder The builder to modify + */ +nix_err nix_flake_settings_add_to_eval_state_builder( + nix_c_context * context, nix_flake_settings * settings, nix_eval_state_builder * builder); + +/** + * @brief A new `nix_flake_reference_parse_flags` with defaults + */ +nix_flake_reference_parse_flags * +nix_flake_reference_parse_flags_new(nix_c_context * context, nix_flake_settings * settings); + +/** + * @brief Deallocate and release the resources associated with a `nix_flake_reference_parse_flags`. + * Does not fail. + * @param[in] flags the `nix_flake_reference_parse_flags *` to free + */ +void nix_flake_reference_parse_flags_free(nix_flake_reference_parse_flags * flags); + +/** + * @brief Provide a base directory for parsing relative flake references + * @param[out] context Optional, stores error information + * @param[in] flags The flags to modify + * @param[in] baseDirectory The base directory to add + * @param[in] baseDirectoryLen The length of baseDirectory + * @return NIX_OK on success, NIX_ERR on failure + */ +nix_err nix_flake_reference_parse_flags_set_base_directory( + nix_c_context * context, + nix_flake_reference_parse_flags * flags, + const char * baseDirectory, + size_t baseDirectoryLen); + +/** + * @brief A new `nix_flake_lock_flags` with defaults + * @param[in] settings Flake settings that may affect the defaults + */ +nix_flake_lock_flags * nix_flake_lock_flags_new(nix_c_context * context, nix_flake_settings * settings); + +/** + * @brief Deallocate and release the resources associated with a `nix_flake_lock_flags`. + * Does not fail. + * @param[in] settings the `nix_flake_lock_flags *` to free + */ +void nix_flake_lock_flags_free(nix_flake_lock_flags * settings); + +/** + * @brief Put the lock flags in a mode that checks whether the lock is up to date. + * @param[out] context Optional, stores error information + * @param[in] flags The flags to modify + * @return NIX_OK on success, NIX_ERR on failure + * + * This causes `nix_flake_lock` to fail if the lock needs to be updated. + */ +nix_err nix_flake_lock_flags_set_mode_check(nix_c_context * context, nix_flake_lock_flags * flags); + +/** + * @brief Put the lock flags in a mode that updates the lock file in memory, if needed. + * @param[out] context Optional, stores error information + * @param[in] flags The flags to modify + * @param[in] update Whether to allow updates + * + * This will cause `nix_flake_lock` to update the lock file in memory, if needed. + */ +nix_err nix_flake_lock_flags_set_mode_virtual(nix_c_context * context, nix_flake_lock_flags * flags); + +/** + * @brief Put the lock flags in a mode that updates the lock file on disk, if needed. + * @param[out] context Optional, stores error information + * @param[in] flags The flags to modify + * @param[in] update Whether to allow updates + * + * This will cause `nix_flake_lock` to update the lock file on disk, if needed. + */ +nix_err nix_flake_lock_flags_set_mode_write_as_needed(nix_c_context * context, nix_flake_lock_flags * flags); + +/** + * @brief Add input overrides to the lock flags + * @param[out] context Optional, stores error information + * @param[in] flags The flags to modify + * @param[in] inputPath The input path to override + * @param[in] flakeRef The flake reference to use as the override + * + * This switches the `flags` to `nix_flake_lock_flags_set_mode_virtual` if not in mode + * `nix_flake_lock_flags_set_mode_check`. + */ +nix_err nix_flake_lock_flags_add_input_override( + nix_c_context * context, nix_flake_lock_flags * flags, const char * inputPath, nix_flake_reference * flakeRef); + +/** + * @brief Lock a flake, if not already locked. + * @param[out] context Optional, stores error information + * @param[in] settings The flake (and fetch) settings to use + * @param[in] flags The locking flags to use + * @param[in] flake The flake to lock + */ +nix_locked_flake * nix_flake_lock( + nix_c_context * context, + nix_fetchers_settings * fetchSettings, + nix_flake_settings * settings, + EvalState * eval_state, + nix_flake_lock_flags * flags, + nix_flake_reference * flake); + +/** + * @brief Deallocate and release the resources associated with a `nix_locked_flake`. + * Does not fail. + * @param[in] locked_flake the `nix_locked_flake *` to free + */ +void nix_locked_flake_free(nix_locked_flake * locked_flake); + +/** + * @brief Parse a URL-like string into a `nix_flake_reference`. + * + * @param[out] context **context** – Optional, stores error information + * @param[in] fetchSettings **context** – The fetch settings to use + * @param[in] flakeSettings **context** – The flake settings to use + * @param[in] parseFlags **context** – Specific context and parameters such as base directory + * + * @param[in] str **input** – The URI-like string to parse + * @param[in] strLen **input** – The length of `str` + * + * @param[out] flakeReferenceOut **result** – The resulting flake reference + * @param[in] fragmentCallback **result** – A callback to call with the fragment part of the URL + * @param[in] fragmentCallbackUserData **result** – User data to pass to the fragment callback + * + * @return NIX_OK on success, NIX_ERR on failure + */ +nix_err nix_flake_reference_and_fragment_from_string( + nix_c_context * context, + nix_fetchers_settings * fetchSettings, + nix_flake_settings * flakeSettings, + nix_flake_reference_parse_flags * parseFlags, + const char * str, + size_t strLen, + nix_flake_reference ** flakeReferenceOut, + nix_get_string_callback fragmentCallback, + void * fragmentCallbackUserData); + +/** + * @brief Deallocate and release the resources associated with a `nix_flake_reference`. + * + * Does not fail. + * + * @param[in] store the `nix_flake_reference *` to free + */ +void nix_flake_reference_free(nix_flake_reference * store); + +/** + * @brief Get the output attributes of a flake. + * @param[out] context Optional, stores error information + * @param[in] settings The settings to use + * @param[in] locked_flake the flake to get the output attributes from + * @return A new nix_value or NULL on failure. Release the `nix_value` with `nix_value_decref`. */ -nix_err nix_flake_init_global(nix_c_context * context, nix_flake_settings * settings); +nix_value * nix_locked_flake_get_output_attrs( + nix_c_context * context, nix_flake_settings * settings, EvalState * evalState, nix_locked_flake * lockedFlake); #ifdef __cplusplus } // extern "C" diff --git a/src/libflake-c/nix_api_flake_internal.hh b/src/libflake-c/nix_api_flake_internal.hh index 4c154a34229..fbc6574d68a 100644 --- a/src/libflake-c/nix_api_flake_internal.hh +++ b/src/libflake-c/nix_api_flake_internal.hh @@ -1,9 +1,32 @@ #pragma once +#include -#include "ref.hh" -#include "flake/settings.hh" +#include "nix/util/ref.hh" +#include "nix/flake/flake.hh" +#include "nix/flake/flakeref.hh" +#include "nix/flake/settings.hh" struct nix_flake_settings { nix::ref settings; }; + +struct nix_flake_reference_parse_flags +{ + std::optional baseDirectory; +}; + +struct nix_flake_reference +{ + nix::ref flakeRef; +}; + +struct nix_flake_lock_flags +{ + nix::ref lockFlags; +}; + +struct nix_locked_flake +{ + nix::ref lockedFlake; +}; diff --git a/src/libflake-c/package.nix b/src/libflake-c/package.nix index a70cbf94ef5..8c6883d9cf9 100644 --- a/src/libflake-c/package.nix +++ b/src/libflake-c/package.nix @@ -1,14 +1,15 @@ -{ lib -, stdenv -, mkMesonLibrary +{ + lib, + mkMesonLibrary, -, nix-store-c -, nix-expr-c -, nix-flake + nix-store-c, + nix-expr-c, + nix-fetchers-c, + nix-flake, -# Configuration Options + # Configuration Options -, version + version, }: let @@ -21,8 +22,8 @@ mkMesonLibrary (finalAttrs: { workDir = ./.; fileset = fileset.unions [ - ../../build-utils-meson - ./build-utils-meson + ../../nix-meson-build-support + ./nix-meson-build-support ../../.version ./.version ./meson.build @@ -35,24 +36,13 @@ mkMesonLibrary (finalAttrs: { propagatedBuildInputs = [ nix-expr-c nix-store-c + nix-fetchers-c nix-flake ]; - preConfigure = - # "Inline" .version so it's not a symlink, and includes the suffix. - # Do the meson utils, without modification. - '' - chmod u+w ./.version - echo ${version} > ../../.version - ''; - mesonFlags = [ ]; - env = lib.optionalAttrs (stdenv.isLinux && !(stdenv.hostPlatform.isStatic && stdenv.system == "aarch64-linux")) { - LDFLAGS = "-fuse-ld=gold"; - }; - meta = { platforms = lib.platforms.unix ++ lib.platforms.windows; }; diff --git a/src/libflake-tests/build-utils-meson b/src/libflake-tests/build-utils-meson deleted file mode 120000 index 5fff21bab55..00000000000 --- a/src/libflake-tests/build-utils-meson +++ /dev/null @@ -1 +0,0 @@ -../../build-utils-meson \ No newline at end of file diff --git a/src/libflake-tests/flakeref.cc b/src/libflake-tests/flakeref.cc index d704a26d36c..1abaffb96a5 100644 --- a/src/libflake-tests/flakeref.cc +++ b/src/libflake-tests/flakeref.cc @@ -1,24 +1,66 @@ #include -#include "fetch-settings.hh" -#include "flake/flakeref.hh" +#include "nix/fetchers/fetch-settings.hh" +#include "nix/flake/flakeref.hh" namespace nix { /* ----------- tests for flake/flakeref.hh --------------------------------------------------*/ - /* ---------------------------------------------------------------------------- - * to_string - * --------------------------------------------------------------------------*/ + TEST(parseFlakeRef, path) { + experimentalFeatureSettings.experimentalFeatures.get().insert(Xp::Flakes); + + fetchers::Settings fetchSettings; + + { + auto s = "/foo/bar"; + auto flakeref = parseFlakeRef(fetchSettings, s); + ASSERT_EQ(flakeref.to_string(), "path:/foo/bar"); + } + + { + auto s = "/foo/bar?revCount=123&rev=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + auto flakeref = parseFlakeRef(fetchSettings, s); + ASSERT_EQ(flakeref.to_string(), "path:/foo/bar?rev=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&revCount=123"); + } + + { + auto s = "/foo/bar?xyzzy=123"; + EXPECT_THROW( + parseFlakeRef(fetchSettings, s), + Error); + } + + { + auto s = "/foo/bar#bla"; + EXPECT_THROW( + parseFlakeRef(fetchSettings, s), + Error); + } + + { + auto s = "/foo/bar#bla"; + auto [flakeref, fragment] = parseFlakeRefWithFragment(fetchSettings, s); + ASSERT_EQ(flakeref.to_string(), "path:/foo/bar"); + ASSERT_EQ(fragment, "bla"); + } + + { + auto s = "/foo/bar?revCount=123#bla"; + auto [flakeref, fragment] = parseFlakeRefWithFragment(fetchSettings, s); + ASSERT_EQ(flakeref.to_string(), "path:/foo/bar?revCount=123"); + ASSERT_EQ(fragment, "bla"); + } + } TEST(to_string, doesntReencodeUrl) { fetchers::Settings fetchSettings; auto s = "http://localhost:8181/test/+3d.tar.gz"; auto flakeref = parseFlakeRef(fetchSettings, s); - auto parsed = flakeref.to_string(); + auto unparsed = flakeref.to_string(); auto expected = "http://localhost:8181/test/%2B3d.tar.gz"; - ASSERT_EQ(parsed, expected); + ASSERT_EQ(unparsed, expected); } } diff --git a/src/libflake-tests/meson.build b/src/libflake-tests/meson.build index c494c414e99..593b0e18d21 100644 --- a/src/libflake-tests/meson.build +++ b/src/libflake-tests/meson.build @@ -4,8 +4,6 @@ project('nix-flake-tests', 'cpp', 'cpp_std=c++2a', # TODO(Qyriad): increase the warning level 'warning_level=1', - 'debug=true', - 'optimization=2', 'errorlogs=true', # Please print logs for tests that fail ], meson_version : '>= 1.1', @@ -14,7 +12,7 @@ project('nix-flake-tests', 'cpp', cxx = meson.get_compiler('cpp') -subdir('build-utils-meson/deps-lists') +subdir('nix-meson-build-support/deps-lists') deps_private_maybe_subproject = [ dependency('nix-expr-test-support'), @@ -23,10 +21,10 @@ deps_private_maybe_subproject = [ ] deps_public_maybe_subproject = [ ] -subdir('build-utils-meson/subprojects') +subdir('nix-meson-build-support/subprojects') -subdir('build-utils-meson/export-all-symbols') -subdir('build-utils-meson/windows-version') +subdir('nix-meson-build-support/export-all-symbols') +subdir('nix-meson-build-support/windows-version') rapidcheck = dependency('rapidcheck') deps_private += rapidcheck @@ -34,16 +32,7 @@ deps_private += rapidcheck gtest = dependency('gtest', main : true) deps_private += gtest -add_project_arguments( - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. - '-include', 'config-util.hh', - '-include', 'config-store.hh', - '-include', 'config-expr.hh', - language : 'cpp', -) - -subdir('build-utils-meson/common') +subdir('nix-meson-build-support/common') sources = files( 'flakeref.cc', diff --git a/src/libflake-tests/nix-meson-build-support b/src/libflake-tests/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/libflake-tests/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/libflake-tests/nix_api_flake.cc b/src/libflake-tests/nix_api_flake.cc index 21109d181a4..f7e0cb71980 100644 --- a/src/libflake-tests/nix_api_flake.cc +++ b/src/libflake-tests/nix_api_flake.cc @@ -1,20 +1,19 @@ +#include "nix/util/file-system.hh" #include "nix_api_store.h" -#include "nix_api_store_internal.h" #include "nix_api_util.h" -#include "nix_api_util_internal.h" #include "nix_api_expr.h" #include "nix_api_value.h" #include "nix_api_flake.h" -#include "tests/nix_api_expr.hh" -#include "tests/string_callback.hh" +#include "nix/expr/tests/nix_api_expr.hh" +#include "nix/util/tests/string_callback.hh" #include #include namespace nixC { -TEST_F(nix_api_store_test, nix_api_init_global_getFlake_exists) +TEST_F(nix_api_store_test, nix_api_init_getFlake_exists) { nix_libstore_init(ctx); assert_ctx_ok(); @@ -25,13 +24,13 @@ TEST_F(nix_api_store_test, nix_api_init_global_getFlake_exists) assert_ctx_ok(); ASSERT_NE(nullptr, settings); - nix_flake_init_global(ctx, settings); - assert_ctx_ok(); - nix_eval_state_builder * builder = nix_eval_state_builder_new(ctx, store); ASSERT_NE(nullptr, builder); assert_ctx_ok(); + nix_flake_settings_add_to_eval_state_builder(ctx, settings, builder); + assert_ctx_ok(); + auto state = nix_eval_state_build(ctx, builder); assert_ctx_ok(); ASSERT_NE(nullptr, state); @@ -43,9 +42,342 @@ TEST_F(nix_api_store_test, nix_api_init_global_getFlake_exists) ASSERT_NE(nullptr, value); nix_err err = nix_expr_eval_from_string(ctx, state, "builtins.getFlake", ".", value); + + nix_state_free(state); + assert_ctx_ok(); ASSERT_EQ(NIX_OK, err); ASSERT_EQ(NIX_TYPE_FUNCTION, nix_get_type(ctx, value)); } +TEST_F(nix_api_store_test, nix_api_flake_reference_not_absolute_no_basedir_fail) +{ + nix_libstore_init(ctx); + assert_ctx_ok(); + nix_libexpr_init(ctx); + assert_ctx_ok(); + + auto settings = nix_flake_settings_new(ctx); + assert_ctx_ok(); + ASSERT_NE(nullptr, settings); + + auto fetchSettings = nix_fetchers_settings_new(ctx); + assert_ctx_ok(); + ASSERT_NE(nullptr, fetchSettings); + + auto parseFlags = nix_flake_reference_parse_flags_new(ctx, settings); + + std::string str(".#legacyPackages.aarch127-unknown...orion"); + std::string fragment; + nix_flake_reference * flakeReference = nullptr; + auto r = nix_flake_reference_and_fragment_from_string( + ctx, fetchSettings, settings, parseFlags, str.data(), str.size(), &flakeReference, OBSERVE_STRING(fragment)); + + ASSERT_NE(NIX_OK, r); + ASSERT_EQ(nullptr, flakeReference); + + nix_flake_reference_parse_flags_free(parseFlags); +} + +TEST_F(nix_api_store_test, nix_api_load_flake) +{ + auto tmpDir = nix::createTempDir(); + nix::AutoDelete delTmpDir(tmpDir, true); + + nix::writeFile(tmpDir + "/flake.nix", R"( + { + outputs = { ... }: { + hello = "potato"; + }; + } + )"); + + nix_libstore_init(ctx); + assert_ctx_ok(); + nix_libexpr_init(ctx); + assert_ctx_ok(); + + auto fetchSettings = nix_fetchers_settings_new(ctx); + assert_ctx_ok(); + ASSERT_NE(nullptr, fetchSettings); + + auto settings = nix_flake_settings_new(ctx); + assert_ctx_ok(); + ASSERT_NE(nullptr, settings); + + nix_eval_state_builder * builder = nix_eval_state_builder_new(ctx, store); + ASSERT_NE(nullptr, builder); + assert_ctx_ok(); + + auto state = nix_eval_state_build(ctx, builder); + assert_ctx_ok(); + ASSERT_NE(nullptr, state); + + nix_eval_state_builder_free(builder); + + auto parseFlags = nix_flake_reference_parse_flags_new(ctx, settings); + assert_ctx_ok(); + ASSERT_NE(nullptr, parseFlags); + + auto r0 = nix_flake_reference_parse_flags_set_base_directory(ctx, parseFlags, tmpDir.c_str(), tmpDir.size()); + assert_ctx_ok(); + ASSERT_EQ(NIX_OK, r0); + + std::string fragment; + const std::string ref = ".#legacyPackages.aarch127-unknown...orion"; + nix_flake_reference * flakeReference = nullptr; + auto r = nix_flake_reference_and_fragment_from_string( + ctx, fetchSettings, settings, parseFlags, ref.data(), ref.size(), &flakeReference, OBSERVE_STRING(fragment)); + assert_ctx_ok(); + ASSERT_EQ(NIX_OK, r); + ASSERT_NE(nullptr, flakeReference); + ASSERT_EQ(fragment, "legacyPackages.aarch127-unknown...orion"); + + nix_flake_reference_parse_flags_free(parseFlags); + + auto lockFlags = nix_flake_lock_flags_new(ctx, settings); + assert_ctx_ok(); + ASSERT_NE(nullptr, lockFlags); + + auto lockedFlake = nix_flake_lock(ctx, fetchSettings, settings, state, lockFlags, flakeReference); + assert_ctx_ok(); + ASSERT_NE(nullptr, lockedFlake); + + nix_flake_lock_flags_free(lockFlags); + + auto value = nix_locked_flake_get_output_attrs(ctx, settings, state, lockedFlake); + assert_ctx_ok(); + ASSERT_NE(nullptr, value); + + auto helloAttr = nix_get_attr_byname(ctx, value, state, "hello"); + assert_ctx_ok(); + ASSERT_NE(nullptr, helloAttr); + + std::string helloStr; + nix_get_string(ctx, helloAttr, OBSERVE_STRING(helloStr)); + assert_ctx_ok(); + ASSERT_EQ("potato", helloStr); + + nix_value_decref(ctx, value); + nix_locked_flake_free(lockedFlake); + nix_flake_reference_free(flakeReference); + nix_state_free(state); + nix_flake_settings_free(settings); +} + +TEST_F(nix_api_store_test, nix_api_load_flake_with_flags) +{ + nix_libstore_init(ctx); + assert_ctx_ok(); + nix_libexpr_init(ctx); + assert_ctx_ok(); + + auto tmpDir = nix::createTempDir(); + nix::AutoDelete delTmpDir(tmpDir, true); + + nix::createDirs(tmpDir + "/b"); + nix::writeFile(tmpDir + "/b/flake.nix", R"( + { + outputs = { ... }: { + hello = "BOB"; + }; + } + )"); + + nix::createDirs(tmpDir + "/a"); + nix::writeFile(tmpDir + "/a/flake.nix", R"( + { + inputs.b.url = ")" + tmpDir + R"(/b"; + outputs = { b, ... }: { + hello = b.hello; + }; + } + )"); + + nix::createDirs(tmpDir + "/c"); + nix::writeFile(tmpDir + "/c/flake.nix", R"( + { + outputs = { ... }: { + hello = "Claire"; + }; + } + )"); + + nix_libstore_init(ctx); + assert_ctx_ok(); + + auto fetchSettings = nix_fetchers_settings_new(ctx); + assert_ctx_ok(); + ASSERT_NE(nullptr, fetchSettings); + + auto settings = nix_flake_settings_new(ctx); + assert_ctx_ok(); + ASSERT_NE(nullptr, settings); + + nix_eval_state_builder * builder = nix_eval_state_builder_new(ctx, store); + ASSERT_NE(nullptr, builder); + assert_ctx_ok(); + + auto state = nix_eval_state_build(ctx, builder); + assert_ctx_ok(); + ASSERT_NE(nullptr, state); + + nix_eval_state_builder_free(builder); + + auto parseFlags = nix_flake_reference_parse_flags_new(ctx, settings); + assert_ctx_ok(); + ASSERT_NE(nullptr, parseFlags); + + auto r0 = nix_flake_reference_parse_flags_set_base_directory(ctx, parseFlags, tmpDir.c_str(), tmpDir.size()); + assert_ctx_ok(); + ASSERT_EQ(NIX_OK, r0); + + std::string fragment; + const std::string ref = "./a"; + nix_flake_reference * flakeReference = nullptr; + auto r = nix_flake_reference_and_fragment_from_string( + ctx, fetchSettings, settings, parseFlags, ref.data(), ref.size(), &flakeReference, OBSERVE_STRING(fragment)); + assert_ctx_ok(); + ASSERT_EQ(NIX_OK, r); + ASSERT_NE(nullptr, flakeReference); + ASSERT_EQ(fragment, ""); + + // Step 1: Do not update, fails + + auto lockFlags = nix_flake_lock_flags_new(ctx, settings); + assert_ctx_ok(); + ASSERT_NE(nullptr, lockFlags); + + nix_flake_lock_flags_set_mode_check(ctx, lockFlags); + assert_ctx_ok(); + + // Step 2: Update but do not write, succeeds + + auto lockedFlake = nix_flake_lock(ctx, fetchSettings, settings, state, lockFlags, flakeReference); + assert_ctx_err(); + ASSERT_EQ(nullptr, lockedFlake); + + nix_flake_lock_flags_set_mode_virtual(ctx, lockFlags); + assert_ctx_ok(); + + lockedFlake = nix_flake_lock(ctx, fetchSettings, settings, state, lockFlags, flakeReference); + assert_ctx_ok(); + ASSERT_NE(nullptr, lockedFlake); + + // Get the output attrs + auto value = nix_locked_flake_get_output_attrs(ctx, settings, state, lockedFlake); + assert_ctx_ok(); + ASSERT_NE(nullptr, value); + + auto helloAttr = nix_get_attr_byname(ctx, value, state, "hello"); + assert_ctx_ok(); + ASSERT_NE(nullptr, helloAttr); + + std::string helloStr; + nix_get_string(ctx, helloAttr, OBSERVE_STRING(helloStr)); + assert_ctx_ok(); + ASSERT_EQ("BOB", helloStr); + + nix_value_decref(ctx, value); + nix_locked_flake_free(lockedFlake); + + // Step 3: Lock was not written, so Step 1 would fail again + + nix_flake_lock_flags_set_mode_check(ctx, lockFlags); + + lockedFlake = nix_flake_lock(ctx, fetchSettings, settings, state, lockFlags, flakeReference); + assert_ctx_err(); + ASSERT_EQ(nullptr, lockedFlake); + + // Step 4: Update and write, succeeds + + nix_flake_lock_flags_set_mode_write_as_needed(ctx, lockFlags); + assert_ctx_ok(); + + lockedFlake = nix_flake_lock(ctx, fetchSettings, settings, state, lockFlags, flakeReference); + assert_ctx_ok(); + ASSERT_NE(nullptr, lockedFlake); + + // Get the output attrs + value = nix_locked_flake_get_output_attrs(ctx, settings, state, lockedFlake); + assert_ctx_ok(); + ASSERT_NE(nullptr, value); + + helloAttr = nix_get_attr_byname(ctx, value, state, "hello"); + assert_ctx_ok(); + ASSERT_NE(nullptr, helloAttr); + + helloStr.clear(); + nix_get_string(ctx, helloAttr, OBSERVE_STRING(helloStr)); + assert_ctx_ok(); + ASSERT_EQ("BOB", helloStr); + + nix_value_decref(ctx, value); + nix_locked_flake_free(lockedFlake); + + // Step 5: Lock was written, so Step 1 would succeed + + nix_flake_lock_flags_set_mode_check(ctx, lockFlags); + assert_ctx_ok(); + + lockedFlake = nix_flake_lock(ctx, fetchSettings, settings, state, lockFlags, flakeReference); + assert_ctx_ok(); + ASSERT_NE(nullptr, lockedFlake); + + // Get the output attrs + value = nix_locked_flake_get_output_attrs(ctx, settings, state, lockedFlake); + assert_ctx_ok(); + ASSERT_NE(nullptr, value); + + helloAttr = nix_get_attr_byname(ctx, value, state, "hello"); + assert_ctx_ok(); + ASSERT_NE(nullptr, helloAttr); + + helloStr.clear(); + nix_get_string(ctx, helloAttr, OBSERVE_STRING(helloStr)); + assert_ctx_ok(); + ASSERT_EQ("BOB", helloStr); + + nix_value_decref(ctx, value); + nix_locked_flake_free(lockedFlake); + + // Step 6: Lock with override, do not write + + nix_flake_lock_flags_set_mode_write_as_needed(ctx, lockFlags); + assert_ctx_ok(); + + nix_flake_reference * overrideFlakeReference = nullptr; + nix_flake_reference_and_fragment_from_string( + ctx, fetchSettings, settings, parseFlags, "./c", 3, &overrideFlakeReference, OBSERVE_STRING(fragment)); + assert_ctx_ok(); + ASSERT_NE(nullptr, overrideFlakeReference); + + nix_flake_lock_flags_add_input_override(ctx, lockFlags, "b", overrideFlakeReference); + assert_ctx_ok(); + + lockedFlake = nix_flake_lock(ctx, fetchSettings, settings, state, lockFlags, flakeReference); + assert_ctx_ok(); + ASSERT_NE(nullptr, lockedFlake); + + // Get the output attrs + value = nix_locked_flake_get_output_attrs(ctx, settings, state, lockedFlake); + assert_ctx_ok(); + ASSERT_NE(nullptr, value); + + helloAttr = nix_get_attr_byname(ctx, value, state, "hello"); + assert_ctx_ok(); + ASSERT_NE(nullptr, helloAttr); + + helloStr.clear(); + nix_get_string(ctx, helloAttr, OBSERVE_STRING(helloStr)); + assert_ctx_ok(); + ASSERT_EQ("Claire", helloStr); + + nix_flake_reference_parse_flags_free(parseFlags); + nix_flake_lock_flags_free(lockFlags); + nix_flake_reference_free(flakeReference); + nix_state_free(state); + nix_flake_settings_free(settings); +} + } // namespace nixC diff --git a/src/libflake-tests/package.nix b/src/libflake-tests/package.nix index b3a8ac46644..714f3791ad9 100644 --- a/src/libflake-tests/package.nix +++ b/src/libflake-tests/package.nix @@ -1,20 +1,21 @@ -{ lib -, buildPackages -, stdenv -, mkMesonExecutable +{ + lib, + buildPackages, + stdenv, + mkMesonExecutable, -, nix-flake -, nix-flake-c -, nix-expr-test-support + nix-flake, + nix-flake-c, + nix-expr-test-support, -, rapidcheck -, gtest -, runCommand + rapidcheck, + gtest, + runCommand, -# Configuration Options + # Configuration Options -, version -, resolvePath + version, + resolvePath, }: let @@ -27,8 +28,8 @@ mkMesonExecutable (finalAttrs: { workDir = ./.; fileset = fileset.unions [ - ../../build-utils-meson - ./build-utils-meson + ../../nix-meson-build-support + ./nix-meson-build-support ../../.version ./.version ./meson.build @@ -45,34 +46,28 @@ mkMesonExecutable (finalAttrs: { gtest ]; - preConfigure = - # "Inline" .version so it's not a symlink, and includes the suffix. - # Do the meson utils, without modification. - '' - chmod u+w ./.version - echo ${version} > ../../.version - ''; - mesonFlags = [ ]; - env = lib.optionalAttrs (stdenv.isLinux && !(stdenv.hostPlatform.isStatic && stdenv.system == "aarch64-linux")) { - LDFLAGS = "-fuse-ld=gold"; - }; - passthru = { tests = { - run = runCommand "${finalAttrs.pname}-run" { - meta.broken = !stdenv.hostPlatform.emulatorAvailable buildPackages; - } (lib.optionalString stdenv.hostPlatform.isWindows '' - export HOME="$PWD/home-dir" - mkdir -p "$HOME" - '' + '' - export _NIX_TEST_UNIT_DATA=${resolvePath ./data} - export NIX_CONFIG="extra-experimental-features = flakes" - ${stdenv.hostPlatform.emulator buildPackages} ${lib.getExe finalAttrs.finalPackage} - touch $out - ''); + run = + runCommand "${finalAttrs.pname}-run" + { + meta.broken = !stdenv.hostPlatform.emulatorAvailable buildPackages; + } + ( + lib.optionalString stdenv.hostPlatform.isWindows '' + export HOME="$PWD/home-dir" + mkdir -p "$HOME" + '' + + '' + export _NIX_TEST_UNIT_DATA=${resolvePath ./data} + export NIX_CONFIG="extra-experimental-features = flakes" + ${stdenv.hostPlatform.emulator buildPackages} ${lib.getExe finalAttrs.finalPackage} + touch $out + '' + ); }; }; diff --git a/src/libflake-tests/url-name.cc b/src/libflake-tests/url-name.cc index 15bc6b11165..c795850f97b 100644 --- a/src/libflake-tests/url-name.cc +++ b/src/libflake-tests/url-name.cc @@ -1,4 +1,4 @@ -#include "flake/url-name.hh" +#include "nix/flake/url-name.hh" #include namespace nix { diff --git a/src/libflake/build-utils-meson b/src/libflake/build-utils-meson deleted file mode 120000 index 5fff21bab55..00000000000 --- a/src/libflake/build-utils-meson +++ /dev/null @@ -1 +0,0 @@ -../../build-utils-meson \ No newline at end of file diff --git a/src/libflake/call-flake.nix b/src/libflake/call-flake.nix new file mode 100644 index 00000000000..ed7947e0601 --- /dev/null +++ b/src/libflake/call-flake.nix @@ -0,0 +1,105 @@ +# This is a helper to callFlake() to lazily fetch flake inputs. + +# The contents of the lock file, in JSON format. +lockFileStr: + +# A mapping of lock file node IDs to { sourceInfo, subdir } attrsets, +# with sourceInfo.outPath providing an SourceAccessor to a previously +# fetched tree. This is necessary for possibly unlocked inputs, in +# particular the root input, but also --override-inputs pointing to +# unlocked trees. +overrides: + +# This is `prim_fetchFinalTree`. +fetchTreeFinal: + +let + inherit (builtins) mapAttrs; + + lockFile = builtins.fromJSON lockFileStr; + + # Resolve a input spec into a node name. An input spec is + # either a node name, or a 'follows' path from the root + # node. + resolveInput = + inputSpec: if builtins.isList inputSpec then getInputByPath lockFile.root inputSpec else inputSpec; + + # Follow an input attrpath (e.g. ["dwarffs" "nixpkgs"]) from the + # root node, returning the final node. + getInputByPath = + nodeName: path: + if path == [ ] then + nodeName + else + getInputByPath + # Since this could be a 'follows' input, call resolveInput. + (resolveInput lockFile.nodes.${nodeName}.inputs.${builtins.head path}) + (builtins.tail path); + + allNodes = mapAttrs ( + key: node: + let + hasOverride = overrides ? ${key}; + isRelative = node.locked.type or null == "path" && builtins.substring 0 1 node.locked.path != "/"; + + parentNode = allNodes.${getInputByPath lockFile.root node.parent}; + + sourceInfo = + if hasOverride then + overrides.${key}.sourceInfo + else if isRelative then + parentNode.sourceInfo + else + # FIXME: remove obsolete node.info. + # Note: lock file entries are always final. + fetchTreeFinal (node.info or { } // removeAttrs node.locked [ "dir" ]); + + subdir = overrides.${key}.dir or node.locked.dir or ""; + + outPath = + if !hasOverride && isRelative then + parentNode.outPath + (if node.locked.path == "" then "" else "/" + node.locked.path) + else + sourceInfo.outPath + (if subdir == "" then "" else "/" + subdir); + + flake = import (outPath + "/flake.nix"); + + inputs = mapAttrs (inputName: inputSpec: allNodes.${resolveInput inputSpec}.result) ( + node.inputs or { } + ); + + outputs = flake.outputs (inputs // { self = result; }); + + result = + outputs + # We add the sourceInfo attribute for its metadata, as they are + # relevant metadata for the flake. However, the outPath of the + # sourceInfo does not necessarily match the outPath of the flake, + # as the flake may be in a subdirectory of a source. + # This is shadowed in the next // + // sourceInfo + // { + # This shadows the sourceInfo.outPath + inherit outPath; + + inherit inputs; + inherit outputs; + inherit sourceInfo; + _type = "flake"; + }; + + in + { + result = + if node.flake or true then + assert builtins.isFunction flake.outputs; + result + else + sourceInfo // { inherit sourceInfo outPath; }; + + inherit outPath sourceInfo; + } + ) lockFile.nodes; + +in +allNodes.${lockFile.root}.result diff --git a/src/libflake/flake/config.cc b/src/libflake/config.cc similarity index 90% rename from src/libflake/flake/config.cc rename to src/libflake/config.cc index 4879de46330..030104e7fe3 100644 --- a/src/libflake/flake/config.cc +++ b/src/libflake/config.cc @@ -1,7 +1,7 @@ -#include "users.hh" -#include "config-global.hh" -#include "flake/settings.hh" -#include "flake.hh" +#include "nix/util/users.hh" +#include "nix/util/config-global.hh" +#include "nix/flake/settings.hh" +#include "nix/flake/flake.hh" #include @@ -32,7 +32,7 @@ static void writeTrustedList(const TrustedList & trustedList) void ConfigFile::apply(const Settings & flakeSettings) { - std::set whitelist{"bash-prompt", "bash-prompt-prefix", "bash-prompt-suffix", "flake-registry", "commit-lock-file-summary", "commit-lockfile-summary"}; + StringSet whitelist{"bash-prompt", "bash-prompt-prefix", "bash-prompt-suffix", "flake-registry", "commit-lock-file-summary", "commit-lockfile-summary"}; for (auto & [name, value] : settings) { diff --git a/src/libflake/flake-primops.cc b/src/libflake/flake-primops.cc new file mode 100644 index 00000000000..7c5ce01b269 --- /dev/null +++ b/src/libflake/flake-primops.cc @@ -0,0 +1,160 @@ +#include "nix/flake/flake-primops.hh" +#include "nix/expr/eval.hh" +#include "nix/flake/flake.hh" +#include "nix/flake/flakeref.hh" +#include "nix/flake/settings.hh" + +namespace nix::flake::primops { + +PrimOp getFlake(const Settings & settings) +{ + auto prim_getFlake = [&settings](EvalState & state, const PosIdx pos, Value ** args, Value & v) { + std::string flakeRefS( + state.forceStringNoCtx(*args[0], pos, "while evaluating the argument passed to builtins.getFlake")); + auto flakeRef = nix::parseFlakeRef(state.fetchSettings, flakeRefS, {}, true); + if (state.settings.pureEval && !flakeRef.input.isLocked()) + throw Error( + "cannot call 'getFlake' on unlocked flake reference '%s', at %s (use --impure to override)", + flakeRefS, + state.positions[pos]); + + callFlake( + state, + lockFlake( + settings, + state, + flakeRef, + LockFlags{ + .updateLockFile = false, + .writeLockFile = false, + .useRegistries = !state.settings.pureEval && settings.useRegistries, + .allowUnlocked = !state.settings.pureEval, + }), + v); + }; + + return PrimOp{ + .name = "__getFlake", + .args = {"args"}, + .doc = R"( + Fetch a flake from a flake reference, and return its output attributes and some metadata. For example: + + ```nix + (builtins.getFlake "nix/55bc52401966fbffa525c574c14f67b00bc4fb3a").packages.x86_64-linux.nix + ``` + + Unless impure evaluation is allowed (`--impure`), the flake reference + must be "locked", e.g. contain a Git revision or content hash. An + example of an unlocked usage is: + + ```nix + (builtins.getFlake "github:edolstra/dwarffs").rev + ``` + )", + .fun = prim_getFlake, + .experimentalFeature = Xp::Flakes, + }; +} + +static void prim_parseFlakeRef(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + std::string flakeRefS( + state.forceStringNoCtx(*args[0], pos, "while evaluating the argument passed to builtins.parseFlakeRef")); + auto attrs = nix::parseFlakeRef(state.fetchSettings, flakeRefS, {}, true).toAttrs(); + auto binds = state.buildBindings(attrs.size()); + for (const auto & [key, value] : attrs) { + auto s = state.symbols.create(key); + auto & vv = binds.alloc(s); + std::visit( + overloaded{ + [&vv](const std::string & value) { vv.mkString(value); }, + [&vv](const uint64_t & value) { vv.mkInt(value); }, + [&vv](const Explicit & value) { vv.mkBool(value.t); }}, + value); + } + v.mkAttrs(binds); +} + +nix::PrimOp parseFlakeRef({ + .name = "__parseFlakeRef", + .args = {"flake-ref"}, + .doc = R"( + Parse a flake reference, and return its exploded form. + + For example: + + ```nix + builtins.parseFlakeRef "github:NixOS/nixpkgs/23.05?dir=lib" + ``` + + evaluates to: + + ```nix + { dir = "lib"; owner = "NixOS"; ref = "23.05"; repo = "nixpkgs"; type = "github"; } + ``` + )", + .fun = prim_parseFlakeRef, + .experimentalFeature = Xp::Flakes, +}); + +static void prim_flakeRefToString(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + state.forceAttrs(*args[0], noPos, "while evaluating the argument passed to builtins.flakeRefToString"); + fetchers::Attrs attrs; + for (const auto & attr : *args[0]->attrs()) { + auto t = attr.value->type(); + if (t == nInt) { + auto intValue = attr.value->integer().value; + + if (intValue < 0) { + state + .error( + "negative value given for flake ref attr %1%: %2%", state.symbols[attr.name], intValue) + .atPos(pos) + .debugThrow(); + } + + attrs.emplace(state.symbols[attr.name], uint64_t(intValue)); + } else if (t == nBool) { + attrs.emplace(state.symbols[attr.name], Explicit{attr.value->boolean()}); + } else if (t == nString) { + attrs.emplace(state.symbols[attr.name], std::string(attr.value->string_view())); + } else { + state + .error( + "flake reference attribute sets may only contain integers, Booleans, " + "and strings, but attribute '%s' is %s", + state.symbols[attr.name], + showType(*attr.value)) + .debugThrow(); + } + } + auto flakeRef = FlakeRef::fromAttrs(state.fetchSettings, attrs); + v.mkString(flakeRef.to_string()); +} + +nix::PrimOp flakeRefToString({ + .name = "__flakeRefToString", + .args = {"attrs"}, + .doc = R"( + Convert a flake reference from attribute set format to URL format. + + For example: + + ```nix + builtins.flakeRefToString { + dir = "lib"; owner = "NixOS"; ref = "23.05"; repo = "nixpkgs"; type = "github"; + } + ``` + + evaluates to + + ```nix + "github:NixOS/nixpkgs/23.05?dir=lib" + ``` + )", + .fun = prim_flakeRefToString, + .experimentalFeature = Xp::Flakes, +}); + +} // namespace nix::flake::primops diff --git a/src/libflake/flake/flake.cc b/src/libflake/flake.cc similarity index 55% rename from src/libflake/flake/flake.cc rename to src/libflake/flake.cc index 19b622a34af..7a11e604788 100644 --- a/src/libflake/flake/flake.cc +++ b/src/libflake/flake.cc @@ -1,17 +1,20 @@ -#include "terminal.hh" -#include "flake.hh" -#include "eval.hh" -#include "eval-settings.hh" -#include "lockfile.hh" -#include "primops.hh" -#include "eval-inline.hh" -#include "store-api.hh" -#include "fetchers.hh" -#include "finally.hh" -#include "fetch-settings.hh" -#include "flake/settings.hh" -#include "value-to-json.hh" -#include "local-fs-store.hh" +#include "nix/util/terminal.hh" +#include "nix/flake/flake.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/eval-settings.hh" +#include "nix/flake/lockfile.hh" +#include "nix/expr/primops.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/store/store-api.hh" +#include "nix/fetchers/fetchers.hh" +#include "nix/util/finally.hh" +#include "nix/fetchers/fetch-settings.hh" +#include "nix/flake/settings.hh" +#include "nix/expr/value-to-json.hh" +#include "nix/store/local-fs-store.hh" +#include "nix/fetchers/fetch-to-store.hh" +#include "nix/util/memory-source-accessor.hh" +#include "nix/fetchers/input-cache.hh" #include @@ -21,62 +24,22 @@ using namespace flake; namespace flake { -typedef std::pair FetchedFlake; -typedef std::vector> FlakeCache; - -static std::optional lookupInFlakeCache( - const FlakeCache & flakeCache, - const FlakeRef & flakeRef) -{ - // FIXME: inefficient. - for (auto & i : flakeCache) { - if (flakeRef == i.first) { - debug("mapping '%s' to previously seen input '%s' -> '%s", - flakeRef, i.first, i.second.second); - return i.second; - } - } - - return std::nullopt; -} - -static std::tuple fetchOrSubstituteTree( +static StorePath copyInputToStore( EvalState & state, - const FlakeRef & originalRef, - bool allowLookup, - FlakeCache & flakeCache) + fetchers::Input & input, + const fetchers::Input & originalInput, + ref accessor) { - auto fetched = lookupInFlakeCache(flakeCache, originalRef); - FlakeRef resolvedRef = originalRef; - - if (!fetched) { - if (originalRef.input.isDirect()) { - fetched.emplace(originalRef.fetchTree(state.store)); - } else { - if (allowLookup) { - resolvedRef = originalRef.resolve(state.store); - auto fetchedResolved = lookupInFlakeCache(flakeCache, originalRef); - if (!fetchedResolved) fetchedResolved.emplace(resolvedRef.fetchTree(state.store)); - flakeCache.push_back({resolvedRef, *fetchedResolved}); - fetched.emplace(*fetchedResolved); - } - else { - throw Error("'%s' is an indirect flake reference, but registry lookups are not allowed", originalRef); - } - } - flakeCache.push_back({originalRef, *fetched}); - } - - auto [storePath, lockedRef] = *fetched; - - debug("got tree '%s' from '%s'", - state.store->printStorePath(storePath), lockedRef); + auto storePath = fetchToStore(*input.settings, *state.store, accessor, FetchMode::Copy, input.getName()); state.allowPath(storePath); - assert(!originalRef.input.getNarHash() || storePath == originalRef.input.computeStorePath(*state.store)); + auto narHash = state.store->queryPathInfo(storePath)->narHash; + input.attrs.insert_or_assign("narHash", narHash.to_string(HashFormat::SRI, true)); - return {std::move(storePath), resolvedRef, lockedRef}; + assert(!originalInput.getNarHash() || storePath == originalInput.computeStorePath(*state.store)); + + return storePath; } static void forceTrivialValue(EvalState & state, Value & value, const PosIdx pos) @@ -94,13 +57,54 @@ static void expectType(EvalState & state, ValueType type, showType(type), showType(value.type()), state.positions[pos]); } -static std::map parseFlakeInputs( - EvalState & state, Value * value, const PosIdx pos, - const std::optional & baseDir, InputPath lockRootPath); +static std::pair, fetchers::Attrs> parseFlakeInputs( + EvalState & state, + Value * value, + const PosIdx pos, + const InputAttrPath & lockRootAttrPath, + const SourcePath & flakeDir, + bool allowSelf); -static FlakeInput parseFlakeInput(EvalState & state, - std::string_view inputName, Value * value, const PosIdx pos, - const std::optional & baseDir, InputPath lockRootPath) +static void parseFlakeInputAttr( + EvalState & state, + const Attr & attr, + fetchers::Attrs & attrs) +{ + // Allow selecting a subset of enum values + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wswitch-enum" + switch (attr.value->type()) { + case nString: + attrs.emplace(state.symbols[attr.name], attr.value->c_str()); + break; + case nBool: + attrs.emplace(state.symbols[attr.name], Explicit { attr.value->boolean() }); + break; + case nInt: { + auto intValue = attr.value->integer().value; + if (intValue < 0) + state.error("negative value given for flake input attribute %1%: %2%", state.symbols[attr.name], intValue).debugThrow(); + attrs.emplace(state.symbols[attr.name], uint64_t(intValue)); + break; + } + default: + if (attr.name == state.symbols.create("publicKeys")) { + experimentalFeatureSettings.require(Xp::VerifiedFetches); + NixStringContext emptyContext = {}; + attrs.emplace(state.symbols[attr.name], printValueAsJSON(state, true, *attr.value, attr.pos, emptyContext).dump()); + } else + state.error("flake input attribute '%s' is %s while a string, Boolean, or integer is expected", + state.symbols[attr.name], showType(*attr.value)).debugThrow(); + } + #pragma GCC diagnostic pop +} + +static FlakeInput parseFlakeInput( + EvalState & state, + Value * value, + const PosIdx pos, + const InputAttrPath & lockRootAttrPath, + const SourcePath & flakeDir) { expectType(state, nAttrs, *value, pos); @@ -117,51 +121,32 @@ static FlakeInput parseFlakeInput(EvalState & state, for (auto & attr : *value->attrs()) { try { if (attr.name == sUrl) { - expectType(state, nString, *attr.value, attr.pos); - url = attr.value->string_view(); + forceTrivialValue(state, *attr.value, pos); + if (attr.value->type() == nString) + url = attr.value->string_view(); + else if (attr.value->type() == nPath) { + auto path = attr.value->path(); + if (path.accessor != flakeDir.accessor) + throw Error("input attribute path '%s' at %s must be in the same source tree as %s", + path, state.positions[attr.pos], flakeDir); + url = "path:" + flakeDir.path.makeRelative(path.path); + } + else + throw Error("expected a string or a path but got %s at %s", + showType(attr.value->type()), state.positions[attr.pos]); attrs.emplace("url", *url); } else if (attr.name == sFlake) { expectType(state, nBool, *attr.value, attr.pos); input.isFlake = attr.value->boolean(); } else if (attr.name == sInputs) { - input.overrides = parseFlakeInputs(state, attr.value, attr.pos, baseDir, lockRootPath); + input.overrides = parseFlakeInputs(state, attr.value, attr.pos, lockRootAttrPath, flakeDir, false).first; } else if (attr.name == sFollows) { expectType(state, nString, *attr.value, attr.pos); - auto follows(parseInputPath(attr.value->c_str())); - follows.insert(follows.begin(), lockRootPath.begin(), lockRootPath.end()); + auto follows(parseInputAttrPath(attr.value->c_str())); + follows.insert(follows.begin(), lockRootAttrPath.begin(), lockRootAttrPath.end()); input.follows = follows; - } else { - // Allow selecting a subset of enum values - #pragma GCC diagnostic push - #pragma GCC diagnostic ignored "-Wswitch-enum" - switch (attr.value->type()) { - case nString: - attrs.emplace(state.symbols[attr.name], attr.value->c_str()); - break; - case nBool: - attrs.emplace(state.symbols[attr.name], Explicit { attr.value->boolean() }); - break; - case nInt: { - auto intValue = attr.value->integer().value; - - if (intValue < 0) { - state.error("negative value given for flake input attribute %1%: %2%", state.symbols[attr.name], intValue).debugThrow(); - } - - attrs.emplace(state.symbols[attr.name], uint64_t(intValue)); - break; - } - default: - if (attr.name == state.symbols.create("publicKeys")) { - experimentalFeatureSettings.require(Xp::VerifiedFetches); - NixStringContext emptyContext = {}; - attrs.emplace(state.symbols[attr.name], printValueAsJSON(state, true, *attr.value, pos, emptyContext).dump()); - } else - state.error("flake input attribute '%s' is %s while a string, Boolean, or integer is expected", - state.symbols[attr.name], showType(*attr.value)).debugThrow(); - } - #pragma GCC diagnostic pop - } + } else + parseFlakeInputAttr(state, attr, attrs); } catch (Error & e) { e.addTrace( state.positions[attr.pos], @@ -182,34 +167,47 @@ static FlakeInput parseFlakeInput(EvalState & state, if (!attrs.empty()) throw Error("unexpected flake input attribute '%s', at %s", attrs.begin()->first, state.positions[pos]); if (url) - input.ref = parseFlakeRef(state.fetchSettings, *url, baseDir, true, input.isFlake); + input.ref = parseFlakeRef(state.fetchSettings, *url, {}, true, input.isFlake, true); } - if (!input.follows && !input.ref) - input.ref = FlakeRef::fromAttrs(state.fetchSettings, {{"type", "indirect"}, {"id", std::string(inputName)}}); + if (input.ref && input.follows) + throw Error("flake input has both a flake reference and a follows attribute, at %s", state.positions[pos]); return input; } -static std::map parseFlakeInputs( - EvalState & state, Value * value, const PosIdx pos, - const std::optional & baseDir, InputPath lockRootPath) +static std::pair, fetchers::Attrs> parseFlakeInputs( + EvalState & state, + Value * value, + const PosIdx pos, + const InputAttrPath & lockRootAttrPath, + const SourcePath & flakeDir, + bool allowSelf) { std::map inputs; + fetchers::Attrs selfAttrs; expectType(state, nAttrs, *value, pos); for (auto & inputAttr : *value->attrs()) { - inputs.emplace(state.symbols[inputAttr.name], - parseFlakeInput(state, - state.symbols[inputAttr.name], - inputAttr.value, - inputAttr.pos, - baseDir, - lockRootPath)); + auto inputName = state.symbols[inputAttr.name]; + if (inputName == "self") { + if (!allowSelf) + throw Error("'self' input attribute not allowed at %s", state.positions[inputAttr.pos]); + expectType(state, nAttrs, *inputAttr.value, inputAttr.pos); + for (auto & attr : *inputAttr.value->attrs()) + parseFlakeInputAttr(state, attr, selfAttrs); + } else { + inputs.emplace(inputName, + parseFlakeInput(state, + inputAttr.value, + inputAttr.pos, + lockRootAttrPath, + flakeDir)); + } } - return inputs; + return {inputs, selfAttrs}; } static Flake readFlake( @@ -218,9 +216,10 @@ static Flake readFlake( const FlakeRef & resolvedRef, const FlakeRef & lockedRef, const SourcePath & rootDir, - const InputPath & lockRootPath) + const InputAttrPath & lockRootAttrPath) { - auto flakePath = rootDir / CanonPath(resolvedRef.subdir) / "flake.nix"; + auto flakeDir = rootDir / CanonPath(resolvedRef.subdir); + auto flakePath = flakeDir / "flake.nix"; // NOTE evalFile forces vInfo to be an attrset because mustBeTrivial is true. Value vInfo; @@ -240,16 +239,19 @@ static Flake readFlake( auto sInputs = state.symbols.create("inputs"); - if (auto inputs = vInfo.attrs()->get(sInputs)) - flake.inputs = parseFlakeInputs(state, inputs->value, inputs->pos, flakePath.parent().path.abs(), lockRootPath); // FIXME + if (auto inputs = vInfo.attrs()->get(sInputs)) { + auto [flakeInputs, selfAttrs] = parseFlakeInputs(state, inputs->value, inputs->pos, lockRootAttrPath, flakeDir, true); + flake.inputs = std::move(flakeInputs); + flake.selfAttrs = std::move(selfAttrs); + } auto sOutputs = state.symbols.create("outputs"); if (auto outputs = vInfo.attrs()->get(sOutputs)) { expectType(state, nFunction, *outputs->value, outputs->pos); - if (outputs->value->isLambda() && outputs->value->payload.lambda.fun->hasFormals()) { - for (auto & formal : outputs->value->payload.lambda.fun->formals->formals) { + if (outputs->value->isLambda() && outputs->value->lambda().fun->hasFormals()) { + for (auto & formal : outputs->value->lambda().fun->formals->formals) { if (formal.name != state.sSelf) flake.inputs.emplace(state.symbols[formal.name], FlakeInput { .ref = parseFlakeRef(state.fetchSettings, std::string(state.symbols[formal.name])) @@ -272,10 +274,10 @@ static Flake readFlake( state.symbols[setting.name], std::string(state.forceStringNoCtx(*setting.value, setting.pos, ""))); else if (setting.value->type() == nPath) { - NixStringContext emptyContext = {}; + auto storePath = fetchToStore(state.fetchSettings, *state.store, setting.value->path(), FetchMode::Copy); flake.config.settings.emplace( state.symbols[setting.name], - state.coerceToString(setting.pos, *setting.value, emptyContext, "", false, true, true).toOwned()); + state.store->printStorePath(storePath)); } else if (setting.value->type() == nInt) flake.config.settings.emplace( @@ -287,7 +289,7 @@ static Flake readFlake( Explicit { state.forceBool(*setting.value, setting.pos, "") }); else if (setting.value->type() == nList) { std::vector ss; - for (auto elem : setting.value->listItems()) { + for (auto elem : setting.value->listView()) { if (elem->type() != nString) state.error("list element in flake configuration setting '%s' is %s while a string is expected", state.symbols[setting.name], showType(*setting.value)).debugThrow(); @@ -313,28 +315,60 @@ static Flake readFlake( return flake; } +static FlakeRef applySelfAttrs( + const FlakeRef & ref, + const Flake & flake) +{ + auto newRef(ref); + + StringSet allowedAttrs{"submodules", "lfs"}; + + for (auto & attr : flake.selfAttrs) { + if (!allowedAttrs.contains(attr.first)) + throw Error("flake 'self' attribute '%s' is not supported", attr.first); + newRef.input.attrs.insert_or_assign(attr.first, attr.second); + } + + return newRef; +} + static Flake getFlake( EvalState & state, const FlakeRef & originalRef, - bool allowLookup, - FlakeCache & flakeCache, - InputPath lockRootPath) + fetchers::UseRegistries useRegistries, + const InputAttrPath & lockRootAttrPath) { - auto [storePath, resolvedRef, lockedRef] = fetchOrSubstituteTree( - state, originalRef, allowLookup, flakeCache); + // Fetch a lazy tree first. + auto cachedInput = state.inputCache->getAccessor(state.store, originalRef.input, useRegistries); - return readFlake(state, originalRef, resolvedRef, lockedRef, state.rootPath(state.store->toRealPath(storePath)), lockRootPath); -} + auto resolvedRef = FlakeRef(std::move(cachedInput.resolvedInput), originalRef.subdir); + auto lockedRef = FlakeRef(std::move(cachedInput.lockedInput), originalRef.subdir); -Flake getFlake(EvalState & state, const FlakeRef & originalRef, bool allowLookup, FlakeCache & flakeCache) -{ - return getFlake(state, originalRef, allowLookup, flakeCache, {}); + // Parse/eval flake.nix to get at the input.self attributes. + auto flake = readFlake(state, originalRef, resolvedRef, lockedRef, {cachedInput.accessor}, lockRootAttrPath); + + // Re-fetch the tree if necessary. + auto newLockedRef = applySelfAttrs(lockedRef, flake); + + if (lockedRef != newLockedRef) { + debug("refetching input '%s' due to self attribute", newLockedRef); + // FIXME: need to remove attrs that are invalidated by the changed input attrs, such as 'narHash'. + newLockedRef.input.attrs.erase("narHash"); + auto cachedInput2 = state.inputCache->getAccessor(state.store, newLockedRef.input, fetchers::UseRegistries::No); + cachedInput.accessor = cachedInput2.accessor; + lockedRef = FlakeRef(std::move(cachedInput2.lockedInput), newLockedRef.subdir); + } + + // Copy the tree to the store. + auto storePath = copyInputToStore(state, lockedRef.input, originalRef.input, cachedInput.accessor); + + // Re-parse flake.nix from the store. + return readFlake(state, originalRef, resolvedRef, lockedRef, state.storePath(storePath), lockRootAttrPath); } -Flake getFlake(EvalState & state, const FlakeRef & originalRef, bool allowLookup) +Flake getFlake(EvalState & state, const FlakeRef & originalRef, fetchers::UseRegistries useRegistries) { - FlakeCache flakeCache; - return getFlake(state, originalRef, allowLookup, flakeCache); + return getFlake(state, originalRef, useRegistries, {}); } static LockFile readLockFile( @@ -356,11 +390,15 @@ LockedFlake lockFlake( { experimentalFeatureSettings.require(Xp::Flakes); - FlakeCache flakeCache; - auto useRegistries = lockFlags.useRegistries.value_or(settings.useRegistries); + auto useRegistriesTop = useRegistries ? fetchers::UseRegistries::All : fetchers::UseRegistries::No; + auto useRegistriesInputs = useRegistries ? fetchers::UseRegistries::Limited : fetchers::UseRegistries::No; - auto flake = getFlake(state, topRef, useRegistries, flakeCache); + auto flake = getFlake( + state, + topRef, + useRegistriesTop, + {}); if (lockFlags.applyNixConfig) { flake.config.apply(settings); @@ -379,13 +417,29 @@ LockedFlake lockFlake( debug("old lock file: %s", oldLockFile); - std::map overrides; - std::set explicitCliOverrides; - std::set overridesUsed, updatesUsed; + struct OverrideTarget + { + FlakeInput input; + SourcePath sourcePath; + std::optional parentInputAttrPath; // FIXME: rename to inputAttrPathPrefix? + }; + + std::map overrides; + std::set explicitCliOverrides; + std::set overridesUsed, updatesUsed; std::map, SourcePath> nodePaths; for (auto & i : lockFlags.inputOverrides) { - overrides.insert_or_assign(i.first, FlakeInput { .ref = i.second }); + overrides.emplace( + i.first, + OverrideTarget { + .input = FlakeInput { .ref = i.second }, + /* Note: any relative overrides + (e.g. `--override-input B/C "path:./foo/bar"`) + are interpreted relative to the top-level + flake. */ + .sourcePath = flake.path, + }); explicitCliOverrides.insert(i.first); } @@ -396,10 +450,10 @@ LockedFlake lockFlake( std::function node, - const InputPath & inputPathPrefix, + const InputAttrPath & inputAttrPathPrefix, std::shared_ptr oldNode, - const InputPath & lockRootPath, - const Path & parentPath, + const InputAttrPath & followsPrefix, + const SourcePath & sourcePath, bool trustLock)> computeLocks; @@ -410,110 +464,170 @@ LockedFlake lockFlake( /* The node whose locks are to be updated.*/ ref node, /* The path to this node in the lock file graph. */ - const InputPath & inputPathPrefix, + const InputAttrPath & inputAttrPathPrefix, /* The old node, if any, from which locks can be copied. */ std::shared_ptr oldNode, - const InputPath & lockRootPath, - const Path & parentPath, + /* The prefix relative to which 'follows' should be + interpreted. When a node is initially locked, it's + relative to the node's flake; when it's already locked, + it's relative to the root of the lock file. */ + const InputAttrPath & followsPrefix, + /* The source path of this node's flake. */ + const SourcePath & sourcePath, bool trustLock) { - debug("computing lock file node '%s'", printInputPath(inputPathPrefix)); + debug("computing lock file node '%s'", printInputAttrPath(inputAttrPathPrefix)); /* Get the overrides (i.e. attributes of the form 'inputs.nixops.inputs.nixpkgs.url = ...'). */ - for (auto & [id, input] : flakeInputs) { + std::function addOverrides; + addOverrides = [&](const FlakeInput & input, const InputAttrPath & prefix) + { for (auto & [idOverride, inputOverride] : input.overrides) { - auto inputPath(inputPathPrefix); - inputPath.push_back(id); - inputPath.push_back(idOverride); - overrides.insert_or_assign(inputPath, inputOverride); + auto inputAttrPath(prefix); + inputAttrPath.push_back(idOverride); + if (inputOverride.ref || inputOverride.follows) + overrides.emplace(inputAttrPath, + OverrideTarget { + .input = inputOverride, + .sourcePath = sourcePath, + .parentInputAttrPath = inputAttrPathPrefix + }); + addOverrides(inputOverride, inputAttrPath); } + }; + + for (auto & [id, input] : flakeInputs) { + auto inputAttrPath(inputAttrPathPrefix); + inputAttrPath.push_back(id); + addOverrides(input, inputAttrPath); } /* Check whether this input has overrides for a non-existent input. */ - for (auto [inputPath, inputOverride] : overrides) { - auto inputPath2(inputPath); - auto follow = inputPath2.back(); - inputPath2.pop_back(); - if (inputPath2 == inputPathPrefix && !flakeInputs.count(follow)) + for (auto [inputAttrPath, inputOverride] : overrides) { + auto inputAttrPath2(inputAttrPath); + auto follow = inputAttrPath2.back(); + inputAttrPath2.pop_back(); + if (inputAttrPath2 == inputAttrPathPrefix && !flakeInputs.count(follow)) warn( "input '%s' has an override for a non-existent input '%s'", - printInputPath(inputPathPrefix), follow); + printInputAttrPath(inputAttrPathPrefix), follow); } /* Go over the flake inputs, resolve/fetch them if necessary (i.e. if they're new or the flakeref changed from what's in the lock file). */ for (auto & [id, input2] : flakeInputs) { - auto inputPath(inputPathPrefix); - inputPath.push_back(id); - auto inputPathS = printInputPath(inputPath); - debug("computing input '%s'", inputPathS); + auto inputAttrPath(inputAttrPathPrefix); + inputAttrPath.push_back(id); + auto inputAttrPathS = printInputAttrPath(inputAttrPath); + debug("computing input '%s'", inputAttrPathS); try { /* Do we have an override for this input from one of the ancestors? */ - auto i = overrides.find(inputPath); + auto i = overrides.find(inputAttrPath); bool hasOverride = i != overrides.end(); - bool hasCliOverride = explicitCliOverrides.contains(inputPath); - if (hasOverride) { - overridesUsed.insert(inputPath); - // Respect the “flakeness†of the input even if we - // override it - i->second.isFlake = input2.isFlake; - } - auto & input = hasOverride ? i->second : input2; + bool hasCliOverride = explicitCliOverrides.contains(inputAttrPath); + if (hasOverride) + overridesUsed.insert(inputAttrPath); + auto input = hasOverride ? i->second.input : input2; + + /* Resolve relative 'path:' inputs relative to + the source path of the overrider. */ + auto overriddenSourcePath = hasOverride ? i->second.sourcePath : sourcePath; + + /* Respect the "flakeness" of the input even if we + override it. */ + if (hasOverride) + input.isFlake = input2.isFlake; /* Resolve 'follows' later (since it may refer to an input path we haven't processed yet. */ if (input.follows) { - InputPath target; + InputAttrPath target; target.insert(target.end(), input.follows->begin(), input.follows->end()); - debug("input '%s' follows '%s'", inputPathS, printInputPath(target)); + debug("input '%s' follows '%s'", inputAttrPathS, printInputAttrPath(target)); node->inputs.insert_or_assign(id, target); continue; } - assert(input.ref); + if (!input.ref) + input.ref = FlakeRef::fromAttrs(state.fetchSettings, {{"type", "indirect"}, {"id", std::string(id)}}); + + auto overriddenParentPath = + input.ref->input.isRelative() + ? std::optional(hasOverride ? i->second.parentInputAttrPath : inputAttrPathPrefix) + : std::nullopt; + + auto resolveRelativePath = [&]() -> std::optional + { + if (auto relativePath = input.ref->input.isRelative()) { + return SourcePath { + overriddenSourcePath.accessor, + CanonPath(*relativePath, overriddenSourcePath.path.parent().value()) + }; + } else + return std::nullopt; + }; + + /* Get the input flake, resolve 'path:./...' + flakerefs relative to the parent flake. */ + auto getInputFlake = [&](const FlakeRef & ref, const fetchers::UseRegistries useRegistries) + { + if (auto resolvedPath = resolveRelativePath()) { + return readFlake(state, ref, ref, ref, *resolvedPath, inputAttrPath); + } else { + return getFlake( + state, + ref, + useRegistries, + inputAttrPath); + } + }; /* Do we have an entry in the existing lock file? And the input is not in updateInputs? */ std::shared_ptr oldLock; - updatesUsed.insert(inputPath); + updatesUsed.insert(inputAttrPath); - if (oldNode && !lockFlags.inputUpdates.count(inputPath)) + if (oldNode && !lockFlags.inputUpdates.count(inputAttrPath)) if (auto oldLock2 = get(oldNode->inputs, id)) if (auto oldLock3 = std::get_if<0>(&*oldLock2)) oldLock = *oldLock3; if (oldLock - && oldLock->originalRef == *input.ref + && oldLock->originalRef.canonicalize() == input.ref->canonicalize() + && oldLock->parentInputAttrPath == overriddenParentPath && !hasCliOverride) { - debug("keeping existing input '%s'", inputPathS); + debug("keeping existing input '%s'", inputAttrPathS); /* Copy the input from the old lock since its flakeref didn't change and there is no override from a higher level flake. */ auto childNode = make_ref( - oldLock->lockedRef, oldLock->originalRef, oldLock->isFlake); + oldLock->lockedRef, + oldLock->originalRef, + oldLock->isFlake, + oldLock->parentInputAttrPath); node->inputs.insert_or_assign(id, childNode); /* If we have this input in updateInputs, then we must fetch the flake to update it. */ - auto lb = lockFlags.inputUpdates.lower_bound(inputPath); + auto lb = lockFlags.inputUpdates.lower_bound(inputAttrPath); auto mustRefetch = lb != lockFlags.inputUpdates.end() - && lb->size() > inputPath.size() - && std::equal(inputPath.begin(), inputPath.end(), lb->begin()); + && lb->size() > inputAttrPath.size() + && std::equal(inputAttrPath.begin(), inputAttrPath.end(), lb->begin()); FlakeInputs fakeInputs; @@ -532,7 +646,7 @@ LockedFlake lockFlake( if (!trustLock) { // It is possible that the flake has changed, // so we must confirm all the follows that are in the lock file are also in the flake. - auto overridePath(inputPath); + auto overridePath(inputAttrPath); overridePath.push_back(i.first); auto o = overrides.find(overridePath); // If the override disappeared, we have to refetch the flake, @@ -544,7 +658,7 @@ LockedFlake lockFlake( break; } } - auto absoluteFollows(lockRootPath); + auto absoluteFollows(followsPrefix); absoluteFollows.insert(absoluteFollows.end(), follows->begin(), follows->end()); fakeInputs.emplace(i.first, FlakeInput { .follows = absoluteFollows, @@ -554,20 +668,23 @@ LockedFlake lockFlake( } if (mustRefetch) { - auto inputFlake = getFlake(state, oldLock->lockedRef, false, flakeCache, inputPath); + auto inputFlake = getInputFlake(oldLock->lockedRef, useRegistriesInputs); nodePaths.emplace(childNode, inputFlake.path.parent()); - computeLocks(inputFlake.inputs, childNode, inputPath, oldLock, lockRootPath, parentPath, false); + computeLocks(inputFlake.inputs, childNode, inputAttrPath, oldLock, followsPrefix, + inputFlake.path, false); } else { - computeLocks(fakeInputs, childNode, inputPath, oldLock, lockRootPath, parentPath, true); + computeLocks(fakeInputs, childNode, inputAttrPath, oldLock, followsPrefix, sourcePath, true); } } else { /* We need to create a new lock file entry. So fetch this input. */ - debug("creating new input '%s'", inputPathS); + debug("creating new input '%s'", inputAttrPathS); - if (!lockFlags.allowUnlocked && !input.ref->input.isLocked()) - throw Error("cannot update unlocked flake input '%s' in pure mode", inputPathS); + if (!lockFlags.allowUnlocked + && !input.ref->input.isLocked() + && !input.ref->input.isRelative()) + throw Error("cannot update unlocked flake input '%s' in pure mode", inputAttrPathS); /* Note: in case of an --override-input, we use the *original* ref (input2.ref) for the @@ -576,20 +693,17 @@ LockedFlake lockFlake( nuked the next time we update the lock file. That is, overrides are sticky unless you use --no-write-lock-file. */ - auto ref = (input2.ref && explicitCliOverrides.contains(inputPath)) ? *input2.ref : *input.ref; + auto inputIsOverride = explicitCliOverrides.contains(inputAttrPath); + auto ref = (input2.ref && inputIsOverride) ? *input2.ref : *input.ref; if (input.isFlake) { - Path localPath = parentPath; - FlakeRef localRef = *input.ref; + auto inputFlake = getInputFlake(*input.ref, inputIsOverride ? fetchers::UseRegistries::All : useRegistriesInputs); - // If this input is a path, recurse it down. - // This allows us to resolve path inputs relative to the current flake. - if (localRef.input.getType() == "path") - localPath = absPath(*input.ref->input.getSourcePath(), parentPath); - - auto inputFlake = getFlake(state, localRef, useRegistries, flakeCache, inputPath); - - auto childNode = make_ref(inputFlake.lockedRef, ref); + auto childNode = make_ref( + inputFlake.lockedRef, + ref, + true, + overriddenParentPath); node->inputs.insert_or_assign(id, childNode); @@ -601,42 +715,52 @@ LockedFlake lockFlake( Finally cleanup([&]() { parents.pop_back(); }); /* Recursively process the inputs of this - flake. Also, unless we already have this flake - in the top-level lock file, use this flake's - own lock file. */ + flake, using its own lock file. */ nodePaths.emplace(childNode, inputFlake.path.parent()); computeLocks( - inputFlake.inputs, childNode, inputPath, - oldLock - ? std::dynamic_pointer_cast(oldLock) - : readLockFile(state.fetchSettings, inputFlake.lockFilePath()).root.get_ptr(), - oldLock ? lockRootPath : inputPath, - localPath, + inputFlake.inputs, childNode, inputAttrPath, + readLockFile(state.fetchSettings, inputFlake.lockFilePath()).root.get_ptr(), + inputAttrPath, + inputFlake.path, false); } else { - auto [storePath, resolvedRef, lockedRef] = fetchOrSubstituteTree( - state, *input.ref, useRegistries, flakeCache); + auto [path, lockedRef] = [&]() -> std::tuple + { + // Handle non-flake 'path:./...' inputs. + if (auto resolvedPath = resolveRelativePath()) { + return {*resolvedPath, *input.ref}; + } else { + auto cachedInput = state.inputCache->getAccessor( + state.store, + input.ref->input, + useRegistriesInputs); + + auto lockedRef = FlakeRef(std::move(cachedInput.lockedInput), input.ref->subdir); + + // FIXME: allow input to be lazy. + auto storePath = copyInputToStore(state, lockedRef.input, input.ref->input, cachedInput.accessor); + + return {state.storePath(storePath), lockedRef}; + } + }(); - auto childNode = make_ref(lockedRef, ref, false); + auto childNode = make_ref(lockedRef, ref, false, overriddenParentPath); - nodePaths.emplace(childNode, state.rootPath(state.store->toRealPath(storePath))); + nodePaths.emplace(childNode, path); node->inputs.insert_or_assign(id, childNode); } } } catch (Error & e) { - e.addTrace({}, "while updating the flake input '%s'", inputPathS); + e.addTrace({}, "while updating the flake input '%s'", inputAttrPathS); throw; } } }; - // Bring in the current ref for relative path resolution if we have it - auto parentPath = flake.path.parent().path.abs(); - nodePaths.emplace(newLockFile.root, flake.path.parent()); computeLocks( @@ -645,17 +769,17 @@ LockedFlake lockFlake( {}, lockFlags.recreateLockFile ? nullptr : oldLockFile.root.get_ptr(), {}, - parentPath, + flake.path, false); for (auto & i : lockFlags.inputOverrides) if (!overridesUsed.count(i.first)) warn("the flag '--override-input %s %s' does not match any input", - printInputPath(i.first), i.second); + printInputAttrPath(i.first), i.second); for (auto & i : lockFlags.inputUpdates) if (!updatesUsed.count(i)) - warn("'%s' does not match any input of this flake", printInputPath(i)); + warn("'%s' does not match any input of this flake", printInputAttrPath(i)); /* Check 'follows' inputs. */ newLockFile.check(); @@ -671,9 +795,13 @@ LockedFlake lockFlake( if (lockFlags.writeLockFile) { if (sourcePath || lockFlags.outputLockFilePath) { - if (auto unlockedInput = newLockFile.isUnlocked()) { + if (auto unlockedInput = newLockFile.isUnlocked(state.fetchSettings)) { + if (lockFlags.failOnUnlocked) + throw Error( + "Not writing lock file of flake '%s' because it has an unlocked input ('%s'). " + "Use '--allow-dirty-locks' to allow this anyway.", topRef, *unlockedInput); if (state.fetchSettings.warnDirty) - warn("will not write lock file of flake '%s' because it has an unlocked input ('%s')", topRef, *unlockedInput); + warn("not writing lock file of flake '%s' because it has an unlocked input ('%s')", topRef, *unlockedInput); } else { if (!lockFlags.updateLockFile) throw Error("flake '%s' requires lock file changes but they're not allowed due to '--no-update-lock-file'", topRef); @@ -686,18 +814,18 @@ LockedFlake lockFlake( writeFile(*lockFlags.outputLockFilePath, newLockFileS); } else { auto relPath = (topRef.subdir == "" ? "" : topRef.subdir + "/") + "flake.lock"; - auto outputLockFilePath = *sourcePath + "/" + relPath; + auto outputLockFilePath = *sourcePath / relPath; bool lockFileExists = pathExists(outputLockFilePath); auto s = chomp(diff); if (lockFileExists) { if (s.empty()) - warn("updating lock file '%s'", outputLockFilePath); + warn("updating lock file %s", outputLockFilePath); else - warn("updating lock file '%s':\n%s", outputLockFilePath, s); + warn("updating lock file %s:\n%s", outputLockFilePath, s); } else - warn("creating lock file '%s': \n%s", outputLockFilePath, s); + warn("creating lock file %s: \n%s", outputLockFilePath, s); std::optional commitMessage = std::nullopt; @@ -724,8 +852,10 @@ LockedFlake lockFlake( repo, so we should re-read it. FIXME: we could also just clear the 'rev' field... */ auto prevLockedRef = flake.lockedRef; - FlakeCache dummyCache; - flake = getFlake(state, topRef, useRegistries, dummyCache); + flake = getFlake( + state, + topRef, + useRegistriesTop); if (lockFlags.commitLockFile && flake.lockedRef.input.getRev() && @@ -752,19 +882,23 @@ LockedFlake lockFlake( } } -std::pair sourcePathToStorePath( - ref store, - const SourcePath & _path) -{ - auto path = _path.path.abs(); +static ref makeInternalFS() { + auto internalFS = make_ref(MemorySourceAccessor {}); + internalFS->setPathDisplay("«flakes-internal»", ""); + internalFS->addFile( + CanonPath("call-flake.nix"), + #include "call-flake.nix.gen.hh" + ); + return internalFS; +} - if (auto store2 = store.dynamic_pointer_cast()) { - auto realStoreDir = store2->getRealStoreDir(); - if (isInDir(path, realStoreDir)) - path = store2->storeDir + path.substr(realStoreDir.size()); - } +static auto internalFS = makeInternalFS(); - return store->toStorePath(path); +static Value * requireInternalFile(EvalState & state, CanonPath path) { + SourcePath p {internalFS, path}; + auto v = state.allocValue(); + state.evalFile(p, *v); // has caching + return v; } void callFlake(EvalState & state, @@ -784,7 +918,7 @@ void callFlake(EvalState & state, auto lockedNode = node.dynamic_pointer_cast(); - auto [storePath, subdir] = sourcePathToStorePath(state.store, sourcePath); + auto [storePath, subdir] = state.store->toStorePath(sourcePath.path.abs()); emitTreeAttrs( state, @@ -806,8 +940,7 @@ void callFlake(EvalState & state, auto & vOverrides = state.allocValue()->mkAttrs(overrides); - auto vCallFlake = state.allocValue(); - state.evalFile(state.callFlakeInternal, *vCallFlake); + Value * vCallFlake = requireInternalFile(state, CanonPath("call-flake.nix")); auto vLocks = state.allocValue(); vLocks->mkString(lockFileStr); @@ -819,160 +952,13 @@ void callFlake(EvalState & state, state.callFunction(*vCallFlake, args, vRes, noPos); } -void initLib(const Settings & settings) -{ - auto prim_getFlake = [&settings](EvalState & state, const PosIdx pos, Value * * args, Value & v) - { - std::string flakeRefS(state.forceStringNoCtx(*args[0], pos, "while evaluating the argument passed to builtins.getFlake")); - auto flakeRef = parseFlakeRef(state.fetchSettings, flakeRefS, {}, true); - if (state.settings.pureEval && !flakeRef.input.isLocked()) - throw Error("cannot call 'getFlake' on unlocked flake reference '%s', at %s (use --impure to override)", flakeRefS, state.positions[pos]); - - callFlake(state, - lockFlake(settings, state, flakeRef, - LockFlags { - .updateLockFile = false, - .writeLockFile = false, - .useRegistries = !state.settings.pureEval && settings.useRegistries, - .allowUnlocked = !state.settings.pureEval, - }), - v); - }; - - RegisterPrimOp::primOps->push_back({ - .name = "__getFlake", - .args = {"args"}, - .doc = R"( - Fetch a flake from a flake reference, and return its output attributes and some metadata. For example: - - ```nix - (builtins.getFlake "nix/55bc52401966fbffa525c574c14f67b00bc4fb3a").packages.x86_64-linux.nix - ``` - - Unless impure evaluation is allowed (`--impure`), the flake reference - must be "locked", e.g. contain a Git revision or content hash. An - example of an unlocked usage is: - - ```nix - (builtins.getFlake "github:edolstra/dwarffs").rev - ``` - )", - .fun = prim_getFlake, - .experimentalFeature = Xp::Flakes, - }); } -static void prim_parseFlakeRef( - EvalState & state, - const PosIdx pos, - Value * * args, - Value & v) -{ - std::string flakeRefS(state.forceStringNoCtx(*args[0], pos, - "while evaluating the argument passed to builtins.parseFlakeRef")); - auto attrs = parseFlakeRef(state.fetchSettings, flakeRefS, {}, true).toAttrs(); - auto binds = state.buildBindings(attrs.size()); - for (const auto & [key, value] : attrs) { - auto s = state.symbols.create(key); - auto & vv = binds.alloc(s); - std::visit(overloaded { - [&vv](const std::string & value) { vv.mkString(value); }, - [&vv](const uint64_t & value) { vv.mkInt(value); }, - [&vv](const Explicit & value) { vv.mkBool(value.t); } - }, value); - } - v.mkAttrs(binds); -} - -static RegisterPrimOp r3({ - .name = "__parseFlakeRef", - .args = {"flake-ref"}, - .doc = R"( - Parse a flake reference, and return its exploded form. - - For example: - - ```nix - builtins.parseFlakeRef "github:NixOS/nixpkgs/23.05?dir=lib" - ``` - - evaluates to: - - ```nix - { dir = "lib"; owner = "NixOS"; ref = "23.05"; repo = "nixpkgs"; type = "github"; } - ``` - )", - .fun = prim_parseFlakeRef, - .experimentalFeature = Xp::Flakes, -}); - - -static void prim_flakeRefToString( - EvalState & state, - const PosIdx pos, - Value * * args, - Value & v) -{ - state.forceAttrs(*args[0], noPos, - "while evaluating the argument passed to builtins.flakeRefToString"); - fetchers::Attrs attrs; - for (const auto & attr : *args[0]->attrs()) { - auto t = attr.value->type(); - if (t == nInt) { - auto intValue = attr.value->integer().value; - - if (intValue < 0) { - state.error("negative value given for flake ref attr %1%: %2%", state.symbols[attr.name], intValue).atPos(pos).debugThrow(); - } - - attrs.emplace(state.symbols[attr.name], uint64_t(intValue)); - } else if (t == nBool) { - attrs.emplace(state.symbols[attr.name], - Explicit { attr.value->boolean() }); - } else if (t == nString) { - attrs.emplace(state.symbols[attr.name], - std::string(attr.value->string_view())); - } else { - state.error( - "flake reference attribute sets may only contain integers, Booleans, " - "and strings, but attribute '%s' is %s", - state.symbols[attr.name], - showType(*attr.value)).debugThrow(); - } - } - auto flakeRef = FlakeRef::fromAttrs(state.fetchSettings, attrs); - v.mkString(flakeRef.to_string()); -} - -static RegisterPrimOp r4({ - .name = "__flakeRefToString", - .args = {"attrs"}, - .doc = R"( - Convert a flake reference from attribute set format to URL format. - - For example: - - ```nix - builtins.flakeRefToString { - dir = "lib"; owner = "NixOS"; ref = "23.05"; repo = "nixpkgs"; type = "github"; - } - ``` - - evaluates to - - ```nix - "github:NixOS/nixpkgs/23.05?dir=lib" - ``` - )", - .fun = prim_flakeRefToString, - .experimentalFeature = Xp::Flakes, -}); - -} - -std::optional LockedFlake::getFingerprint(ref store) const +std::optional LockedFlake::getFingerprint( + ref store, + const fetchers::Settings & fetchSettings) const { - if (lockFile.isUnlocked()) return std::nullopt; + if (lockFile.isUnlocked(fetchSettings)) return std::nullopt; auto fingerprint = flake.lockedRef.input.getFingerprint(store); if (!fingerprint) return std::nullopt; diff --git a/src/libflake/flake/settings.cc b/src/libflake/flake/settings.cc deleted file mode 100644 index 6a0294e6229..00000000000 --- a/src/libflake/flake/settings.cc +++ /dev/null @@ -1,7 +0,0 @@ -#include "flake/settings.hh" - -namespace nix::flake { - -Settings::Settings() {} - -} diff --git a/src/libflake/flake/flakeref.cc b/src/libflake/flakeref.cc similarity index 69% rename from src/libflake/flake/flakeref.cc rename to src/libflake/flakeref.cc index 9616fe0eaff..37b7eff4ccb 100644 --- a/src/libflake/flake/flakeref.cc +++ b/src/libflake/flakeref.cc @@ -1,9 +1,8 @@ -#include "flakeref.hh" -#include "store-api.hh" -#include "url.hh" -#include "url-parts.hh" -#include "fetchers.hh" -#include "registry.hh" +#include "nix/flake/flakeref.hh" +#include "nix/store/store-api.hh" +#include "nix/util/url.hh" +#include "nix/util/url-parts.hh" +#include "nix/fetchers/fetchers.hh" namespace nix { @@ -16,7 +15,7 @@ const static std::string subDirRegex = subDirElemRegex + "(?:/" + subDirElemRege std::string FlakeRef::to_string() const { - std::map extraQuery; + StringMap extraQuery; if (subdir != "") extraQuery.insert_or_assign("dir", subdir); return input.toURLString(extraQuery); @@ -36,9 +35,11 @@ std::ostream & operator << (std::ostream & str, const FlakeRef & flakeRef) return str; } -FlakeRef FlakeRef::resolve(ref store) const +FlakeRef FlakeRef::resolve( + ref store, + fetchers::UseRegistries useRegistries) const { - auto [input2, extraAttrs] = lookupInRegistries(store, input); + auto [input2, extraAttrs] = lookupInRegistries(store, input, useRegistries); return FlakeRef(std::move(input2), fetchers::maybeGetStrAttr(extraAttrs, "dir").value_or(subdir)); } @@ -47,26 +48,15 @@ FlakeRef parseFlakeRef( const std::string & url, const std::optional & baseDir, bool allowMissing, - bool isFlake) + bool isFlake, + bool preserveRelativePaths) { - auto [flakeRef, fragment] = parseFlakeRefWithFragment(fetchSettings, url, baseDir, allowMissing, isFlake); + auto [flakeRef, fragment] = parseFlakeRefWithFragment(fetchSettings, url, baseDir, allowMissing, isFlake, preserveRelativePaths); if (fragment != "") throw Error("unexpected fragment '%s' in flake reference '%s'", fragment, url); return flakeRef; } -std::optional maybeParseFlakeRef( - const fetchers::Settings & fetchSettings, - const std::string & url, - const std::optional & baseDir) -{ - try { - return parseFlakeRef(fetchSettings, url, baseDir); - } catch (Error &) { - return {}; - } -} - static std::pair fromParsedURL( const fetchers::Settings & fetchSettings, ParsedURL && parsedURL, @@ -86,32 +76,26 @@ std::pair parsePathFlakeRefWithFragment( const std::string & url, const std::optional & baseDir, bool allowMissing, - bool isFlake) + bool isFlake, + bool preserveRelativePaths) { - std::string path = url; - std::string fragment = ""; - std::map query; - auto pathEnd = url.find_first_of("#?"); - auto fragmentStart = pathEnd; - if (pathEnd != std::string::npos && url[pathEnd] == '?') { - fragmentStart = url.find("#"); - } - if (pathEnd != std::string::npos) { - path = url.substr(0, pathEnd); - } - if (fragmentStart != std::string::npos) { - fragment = percentDecode(url.substr(fragmentStart+1)); - } - if (pathEnd != std::string::npos && fragmentStart != std::string::npos && url[pathEnd] == '?') { - query = decodeQuery(url.substr(pathEnd + 1, fragmentStart - pathEnd - 1)); - } + static std::regex pathFlakeRegex( + R"(([^?#]*)(\?([^#]*))?(#(.*))?)", + std::regex::ECMAScript); + + std::smatch match; + auto succeeds = std::regex_match(url, match, pathFlakeRegex); + assert(succeeds); + auto path = match[1].str(); + auto query = decodeQuery(match[3]); + auto fragment = percentDecode(match[5].str()); if (baseDir) { /* Check if 'url' is a path (either absolute or relative - to 'baseDir'). If so, search upward to the root of the - repo (i.e. the directory containing .git). */ + to 'baseDir'). If so, search upward to the root of the + repo (i.e. the directory containing .git). */ - path = absPath(path, baseDir); + path = absPath(path, baseDir, true); if (isFlake) { @@ -158,11 +142,7 @@ std::pair parsePathFlakeRefWithFragment( while (flakeRoot != "/") { if (pathExists(flakeRoot + "/.git")) { - auto base = std::string("git+file://") + flakeRoot; - auto parsedURL = ParsedURL{ - .url = base, // FIXME - .base = base, .scheme = "git+file", .authority = "", .path = flakeRoot, @@ -188,16 +168,17 @@ std::pair parsePathFlakeRefWithFragment( } } else { - if (!hasPrefix(path, "/")) + if (!preserveRelativePaths && !isAbsolute(path)) throw BadURL("flake reference '%s' is not an absolute path", url); - path = canonPath(path + "/" + getOr(query, "dir", "")); } - fetchers::Attrs attrs; - attrs.insert_or_assign("type", "path"); - attrs.insert_or_assign("path", path); - - return std::make_pair(FlakeRef(fetchers::Input::fromAttrs(fetchSettings, std::move(attrs)), ""), fragment); + return fromParsedURL(fetchSettings, { + .scheme = "path", + .authority = "", + .path = path, + .query = query, + .fragment = fragment + }, isFlake); } /** @@ -207,8 +188,7 @@ std::pair parsePathFlakeRefWithFragment( static std::optional> parseFlakeIdRef( const fetchers::Settings & fetchSettings, const std::string & url, - bool isFlake -) + bool isFlake) { std::smatch match; @@ -219,8 +199,6 @@ static std::optional> parseFlakeIdRef( if (std::regex_match(url, match, flakeRegex)) { auto parsedURL = ParsedURL{ - .url = url, - .base = "flake:" + match.str(1), .scheme = "flake", .authority = "", .path = match[1], @@ -238,11 +216,15 @@ std::optional> parseURLFlakeRef( const fetchers::Settings & fetchSettings, const std::string & url, const std::optional & baseDir, - bool isFlake -) + bool isFlake) { try { - return fromParsedURL(fetchSettings, parseURL(url), isFlake); + auto parsed = parseURL(url); + if (baseDir + && (parsed.scheme == "path" || parsed.scheme == "git+file") + && !isAbsolute(parsed.path)) + parsed.path = absPath(parsed.path, *baseDir); + return fromParsedURL(fetchSettings, std::move(parsed), isFlake); } catch (BadURL &) { return std::nullopt; } @@ -253,7 +235,8 @@ std::pair parseFlakeRefWithFragment( const std::string & url, const std::optional & baseDir, bool allowMissing, - bool isFlake) + bool isFlake, + bool preserveRelativePaths) { using namespace fetchers; @@ -262,18 +245,7 @@ std::pair parseFlakeRefWithFragment( } else if (auto res = parseURLFlakeRef(fetchSettings, url, baseDir, isFlake)) { return *res; } else { - return parsePathFlakeRefWithFragment(fetchSettings, url, baseDir, allowMissing, isFlake); - } -} - -std::optional> maybeParseFlakeRefWithFragment( - const fetchers::Settings & fetchSettings, - const std::string & url, const std::optional & baseDir) -{ - try { - return parseFlakeRefWithFragment(fetchSettings, url, baseDir); - } catch (Error & e) { - return {}; + return parsePathFlakeRefWithFragment(fetchSettings, url, baseDir, allowMissing, isFlake, preserveRelativePaths); } } @@ -288,10 +260,59 @@ FlakeRef FlakeRef::fromAttrs( fetchers::maybeGetStrAttr(attrs, "dir").value_or("")); } -std::pair FlakeRef::fetchTree(ref store) const +std::pair, FlakeRef> FlakeRef::lazyFetch(ref store) const { - auto [storePath, lockedInput] = input.fetchToStore(store); - return {std::move(storePath), FlakeRef(std::move(lockedInput), subdir)}; + auto [accessor, lockedInput] = input.getAccessor(store); + return {accessor, FlakeRef(std::move(lockedInput), subdir)}; +} + +FlakeRef FlakeRef::canonicalize() const +{ + auto flakeRef(*this); + + /* Backward compatibility hack: In old versions of Nix, if you had + a flake input like + + inputs.foo.url = "git+https://foo/bar?dir=subdir"; + + it would result in a lock file entry like + + "original": { + "dir": "subdir", + "type": "git", + "url": "https://foo/bar?dir=subdir" + } + + New versions of Nix remove `?dir=subdir` from the `url` field, + since the subdirectory is intended for `FlakeRef`, not the + fetcher (and specifically the remote server), that is, the + flakeref is parsed into + + "original": { + "dir": "subdir", + "type": "git", + "url": "https://foo/bar" + } + + However, this causes new versions of Nix to consider the lock + file entry to be stale since the `original` ref no longer + matches exactly. + + For this reason, we canonicalise the `original` ref by + filtering the `dir` query parameter from the URL. */ + if (auto url = fetchers::maybeGetStrAttr(flakeRef.input.attrs, "url")) { + try { + auto parsed = parseURL(*url); + if (auto dir2 = get(parsed.query, "dir")) { + if (flakeRef.subdir != "" && flakeRef.subdir == *dir2) + parsed.query.erase("dir"); + } + flakeRef.input.attrs.insert_or_assign("url", parsed.to_string()); + } catch (BadURL &) { + } + } + + return flakeRef; } std::tuple parseFlakeRefWithFragmentAndExtendedOutputsSpec( diff --git a/src/libflake/include/nix/flake/flake-primops.hh b/src/libflake/include/nix/flake/flake-primops.hh new file mode 100644 index 00000000000..e7b86b9b31d --- /dev/null +++ b/src/libflake/include/nix/flake/flake-primops.hh @@ -0,0 +1,16 @@ +#pragma once + +#include "nix/expr/eval.hh" +#include "nix/flake/settings.hh" + +namespace nix::flake::primops { + +/** + * Returns a `builtins.getFlake` primop with the given nix::flake::Settings. + */ +nix::PrimOp getFlake(const Settings & settings); + +extern nix::PrimOp parseFlakeRef; +extern nix::PrimOp flakeRefToString; + +} // namespace nix::flake diff --git a/src/libflake/flake/flake.hh b/src/libflake/include/nix/flake/flake.hh similarity index 84% rename from src/libflake/flake/flake.hh rename to src/libflake/include/nix/flake/flake.hh index cc2bea76e59..ed34aa9c8db 100644 --- a/src/libflake/flake/flake.hh +++ b/src/libflake/include/nix/flake/flake.hh @@ -1,10 +1,10 @@ #pragma once ///@file -#include "types.hh" -#include "flakeref.hh" -#include "lockfile.hh" -#include "value.hh" +#include "nix/util/types.hh" +#include "nix/flake/flakeref.hh" +#include "nix/flake/lockfile.hh" +#include "nix/expr/value.hh" namespace nix { @@ -14,14 +14,6 @@ namespace flake { struct Settings; -/** - * Initialize `libnixflake` - * - * So far, this registers the `builtins.getFlake` primop, which depends - * on the choice of `flake:Settings`. - */ -void initLib(const Settings & settings); - struct FlakeInput; typedef std::map FlakeInputs; @@ -57,7 +49,7 @@ struct FlakeInput * false = (fetched) static source path */ bool isFlake = true; - std::optional follows; + std::optional follows; FlakeInputs overrides; }; @@ -71,7 +63,7 @@ struct ConfigFile }; /** - * The contents of a flake.nix file. + * A flake in context */ struct Flake { @@ -79,24 +71,37 @@ struct Flake * The original flake specification (by the user) */ FlakeRef originalRef; + /** * registry references and caching resolved to the specific underlying flake */ FlakeRef resolvedRef; + /** * the specific local store result of invoking the fetcher */ FlakeRef lockedRef; + /** * The path of `flake.nix`. */ SourcePath path; + /** - * pretend that 'lockedRef' is dirty + * Pretend that `lockedRef` is dirty. */ bool forceDirty = false; + std::optional description; + FlakeInputs inputs; + + /** + * Attributes to be retroactively applied to the `self` input + * (such as `submodules = true`). + */ + fetchers::Attrs selfAttrs; + /** * 'nixConfig' attribute */ @@ -110,7 +115,7 @@ struct Flake } }; -Flake getFlake(EvalState & state, const FlakeRef & flakeRef, bool allowLookup); +Flake getFlake(EvalState & state, const FlakeRef & flakeRef, fetchers::UseRegistries useRegistries); /** * Fingerprint of a locked flake; used as a cache key. @@ -124,12 +129,14 @@ struct LockedFlake /** * Source tree accessors for nodes that have been fetched in - * lockFlake(); in particular, the root node and the overriden + * lockFlake(); in particular, the root node and the overridden * inputs. */ std::map, SourcePath> nodePaths; - std::optional getFingerprint(ref store) const; + std::optional getFingerprint( + ref store, + const fetchers::Settings & fetchSettings) const; }; struct LockFlags @@ -156,6 +163,11 @@ struct LockFlags */ bool writeLockFile = true; + /** + * Throw an exception when the flake has an unlocked input. + */ + bool failOnUnlocked = false; + /** * Whether to use the registries to lookup indirect flake * references like 'nixpkgs'. @@ -194,13 +206,13 @@ struct LockFlags /** * Flake inputs to be overridden. */ - std::map inputOverrides; + std::map inputOverrides; /** * Flake inputs to be updated. This means that any existing lock * for those inputs will be ignored. */ - std::set inputUpdates; + std::set inputUpdates; }; LockedFlake lockFlake( @@ -214,16 +226,6 @@ void callFlake( const LockedFlake & lockedFlake, Value & v); -/** - * Map a `SourcePath` to the corresponding store path. This is a - * temporary hack to support chroot stores while we don't have full - * lazy trees. FIXME: Remove this once we can pass a sourcePath rather - * than a storePath to call-flake.nix. - */ -std::pair sourcePathToStorePath( - ref store, - const SourcePath & path); - } void emitTreeAttrs( diff --git a/src/libflake/flake/flakeref.hh b/src/libflake/include/nix/flake/flakeref.hh similarity index 77% rename from src/libflake/flake/flakeref.hh rename to src/libflake/include/nix/flake/flakeref.hh index 1064538a72a..c0045fcf368 100644 --- a/src/libflake/flake/flakeref.hh +++ b/src/libflake/include/nix/flake/flakeref.hh @@ -3,9 +3,10 @@ #include -#include "types.hh" -#include "fetchers.hh" -#include "outputs-spec.hh" +#include "nix/util/types.hh" +#include "nix/fetchers/fetchers.hh" +#include "nix/store/outputs-spec.hh" +#include "nix/fetchers/registry.hh" namespace nix { @@ -48,6 +49,11 @@ struct FlakeRef bool operator ==(const FlakeRef & other) const = default; + bool operator <(const FlakeRef & other) const + { + return std::tie(input, subdir) < std::tie(other.input, other.subdir); + } + FlakeRef(fetchers::Input && input, const Path & subdir) : input(std::move(input)), subdir(subdir) { } @@ -57,13 +63,21 @@ struct FlakeRef fetchers::Attrs toAttrs() const; - FlakeRef resolve(ref store) const; + FlakeRef resolve( + ref store, + fetchers::UseRegistries useRegistries = fetchers::UseRegistries::All) const; static FlakeRef fromAttrs( const fetchers::Settings & fetchSettings, const fetchers::Attrs & attrs); - std::pair fetchTree(ref store) const; + std::pair, FlakeRef> lazyFetch(ref store) const; + + /** + * Canonicalize a flakeref for the purpose of comparing "old" and + * "new" `original` fields in lock files. + */ + FlakeRef canonicalize() const; }; std::ostream & operator << (std::ostream & str, const FlakeRef & flakeRef); @@ -76,15 +90,8 @@ FlakeRef parseFlakeRef( const std::string & url, const std::optional & baseDir = {}, bool allowMissing = false, - bool isFlake = true); - -/** - * @param baseDir Optional [base directory](https://nixos.org/manual/nix/unstable/glossary#gloss-base-directory) - */ -std::optional maybeParseFlake( - const fetchers::Settings & fetchSettings, - const std::string & url, - const std::optional & baseDir = {}); + bool isFlake = true, + bool preserveRelativePaths = false); /** * @param baseDir Optional [base directory](https://nixos.org/manual/nix/unstable/glossary#gloss-base-directory) @@ -94,15 +101,8 @@ std::pair parseFlakeRefWithFragment( const std::string & url, const std::optional & baseDir = {}, bool allowMissing = false, - bool isFlake = true); - -/** - * @param baseDir Optional [base directory](https://nixos.org/manual/nix/unstable/glossary#gloss-base-directory) - */ -std::optional> maybeParseFlakeRefWithFragment( - const fetchers::Settings & fetchSettings, - const std::string & url, - const std::optional & baseDir = {}); + bool isFlake = true, + bool preserveRelativePaths = false); /** * @param baseDir Optional [base directory](https://nixos.org/manual/nix/unstable/glossary#gloss-base-directory) diff --git a/src/libflake/flake/lockfile.hh b/src/libflake/include/nix/flake/lockfile.hh similarity index 64% rename from src/libflake/flake/lockfile.hh rename to src/libflake/include/nix/flake/lockfile.hh index a2711a51652..97bd7a49538 100644 --- a/src/libflake/flake/lockfile.hh +++ b/src/libflake/include/nix/flake/lockfile.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "flakeref.hh" +#include "nix/flake/flakeref.hh" #include @@ -12,7 +12,7 @@ class StorePath; namespace nix::flake { -typedef std::vector InputPath; +typedef std::vector InputAttrPath; struct LockedNode; @@ -23,7 +23,7 @@ struct LockedNode; */ struct Node : std::enable_shared_from_this { - typedef std::variant, InputPath> Edge; + typedef std::variant, InputAttrPath> Edge; std::map inputs; @@ -38,11 +38,19 @@ struct LockedNode : Node FlakeRef lockedRef, originalRef; bool isFlake = true; + /* The node relative to which relative source paths + (e.g. 'path:../foo') are interpreted. */ + std::optional parentInputAttrPath; + LockedNode( const FlakeRef & lockedRef, const FlakeRef & originalRef, - bool isFlake = true) - : lockedRef(lockedRef), originalRef(originalRef), isFlake(isFlake) + bool isFlake = true, + std::optional parentInputAttrPath = {}) + : lockedRef(std::move(lockedRef)) + , originalRef(std::move(originalRef)) + , isFlake(isFlake) + , parentInputAttrPath(std::move(parentInputAttrPath)) { } LockedNode( @@ -71,13 +79,13 @@ struct LockFile * Check whether this lock file has any unlocked or non-final * inputs. If so, return one. */ - std::optional isUnlocked() const; + std::optional isUnlocked(const fetchers::Settings & fetchSettings) const; bool operator ==(const LockFile & other) const; - std::shared_ptr findInput(const InputPath & path); + std::shared_ptr findInput(const InputAttrPath & path); - std::map getAllInputs() const; + std::map getAllInputs() const; static std::string diff(const LockFile & oldLocks, const LockFile & newLocks); @@ -89,8 +97,8 @@ struct LockFile std::ostream & operator <<(std::ostream & stream, const LockFile & lockFile); -InputPath parseInputPath(std::string_view s); +InputAttrPath parseInputAttrPath(std::string_view s); -std::string printInputPath(const InputPath & path); +std::string printInputAttrPath(const InputAttrPath & path); } diff --git a/src/libflake/include/nix/flake/meson.build b/src/libflake/include/nix/flake/meson.build new file mode 100644 index 00000000000..ece1ad4ea33 --- /dev/null +++ b/src/libflake/include/nix/flake/meson.build @@ -0,0 +1,11 @@ +# Public headers directory + +include_dirs = [include_directories('../..')] + +headers = files( + 'flake.hh', + 'flakeref.hh', + 'lockfile.hh', + 'settings.hh', + 'url-name.hh', +) diff --git a/src/libflake/flake/settings.hh b/src/libflake/include/nix/flake/settings.hh similarity index 75% rename from src/libflake/flake/settings.hh rename to src/libflake/include/nix/flake/settings.hh index fee247a7d2f..b3bffad4ccf 100644 --- a/src/libflake/flake/settings.hh +++ b/src/libflake/include/nix/flake/settings.hh @@ -1,21 +1,24 @@ #pragma once ///@file -#include "types.hh" -#include "config.hh" -#include "util.hh" - -#include -#include +#include "nix/util/configuration.hh" #include +namespace nix { +// Forward declarations +struct EvalSettings; + +} // namespace nix + namespace nix::flake { struct Settings : public Config { Settings(); + void configureEvalSettings(nix::EvalSettings & evalSettings) const; + Setting useRegistries{ this, true, @@ -29,7 +32,7 @@ struct Settings : public Config this, false, "accept-flake-config", - "Whether to accept nix configuration from a flake without prompting.", + "Whether to accept Nix configuration settings from a flake without prompting.", {}, true, Xp::Flakes}; diff --git a/src/libflake/flake/url-name.hh b/src/libflake/include/nix/flake/url-name.hh similarity index 83% rename from src/libflake/flake/url-name.hh rename to src/libflake/include/nix/flake/url-name.hh index 6f32754d268..d295ca8f8d4 100644 --- a/src/libflake/flake/url-name.hh +++ b/src/libflake/include/nix/flake/url-name.hh @@ -1,7 +1,7 @@ -#include "url.hh" -#include "url-parts.hh" -#include "util.hh" -#include "split.hh" +#include "nix/util/url.hh" +#include "nix/util/url-parts.hh" +#include "nix/util/util.hh" +#include "nix/util/split.hh" namespace nix { diff --git a/src/libflake/flake/lockfile.cc b/src/libflake/lockfile.cc similarity index 75% rename from src/libflake/flake/lockfile.cc rename to src/libflake/lockfile.cc index 668ed165fbc..646516caf2a 100644 --- a/src/libflake/flake/lockfile.cc +++ b/src/libflake/lockfile.cc @@ -1,7 +1,10 @@ #include -#include "lockfile.hh" -#include "store-api.hh" +#include "nix/fetchers/fetch-settings.hh" +#include "nix/flake/settings.hh" +#include "nix/flake/lockfile.hh" +#include "nix/store/store-api.hh" +#include "nix/util/strings.hh" #include #include @@ -9,7 +12,6 @@ #include #include -#include "strings.hh" namespace nix::flake { @@ -42,10 +44,18 @@ LockedNode::LockedNode( : lockedRef(getFlakeRef(fetchSettings, json, "locked", "info")) // FIXME: remove "info" , originalRef(getFlakeRef(fetchSettings, json, "original", nullptr)) , isFlake(json.find("flake") != json.end() ? (bool) json["flake"] : true) + , parentInputAttrPath(json.find("parent") != json.end() ? (std::optional) json["parent"] : std::nullopt) { - if (!lockedRef.input.isLocked()) - throw Error("lock file contains unlocked input '%s'", - fetchers::attrsToJSON(lockedRef.input.toAttrs())); + if (!lockedRef.input.isLocked() && !lockedRef.input.isRelative()) { + if (lockedRef.input.getNarHash()) + warn( + "Lock file entry '%s' is unlocked (e.g. lacks a Git revision) but does have a NAR hash. " + "This is deprecated since such inputs are verifiable but may not be reproducible.", + lockedRef.to_string()); + else + throw Error("Lock file contains unlocked input '%s'. Use '--allow-dirty-locks' to accept this lock file.", + fetchers::attrsToJSON(lockedRef.input.toAttrs())); + } // For backward compatibility, lock file entries are implicitly final. assert(!lockedRef.input.attrs.contains("__final")); @@ -57,7 +67,7 @@ StorePath LockedNode::computeStorePath(Store & store) const return lockedRef.input.computeStorePath(store); } -static std::shared_ptr doFind(const ref & root, const InputPath & path, std::vector & visited) +static std::shared_ptr doFind(const ref & root, const InputAttrPath & path, std::vector & visited) { auto pos = root; @@ -65,8 +75,8 @@ static std::shared_ptr doFind(const ref & root, const InputPath & pa if (found != visited.end()) { std::vector cycle; - std::transform(found, visited.cend(), std::back_inserter(cycle), printInputPath); - cycle.push_back(printInputPath(path)); + std::transform(found, visited.cend(), std::back_inserter(cycle), printInputAttrPath); + cycle.push_back(printInputAttrPath(path)); throw Error("follow cycle detected: [%s]", concatStringsSep(" -> ", cycle)); } visited.push_back(path); @@ -88,9 +98,9 @@ static std::shared_ptr doFind(const ref & root, const InputPath & pa return pos; } -std::shared_ptr LockFile::findInput(const InputPath & path) +std::shared_ptr LockFile::findInput(const InputAttrPath & path) { - std::vector visited; + std::vector visited; return doFind(root, path, visited); } @@ -98,8 +108,13 @@ LockFile::LockFile( const fetchers::Settings & fetchSettings, std::string_view contents, std::string_view path) { - auto json = nlohmann::json::parse(contents); - + auto json = [=] { + try { + return nlohmann::json::parse(contents); + } catch (const nlohmann::json::parse_error & e) { + throw Error("Could not parse '%s': %s", path, e.what()); + } + }(); auto version = json.value("version", 0); if (version < 5 || version > 7) throw Error("lock file '%s' has unsupported version %d", path, version); @@ -113,7 +128,7 @@ LockFile::LockFile( if (jsonNode.find("inputs") == jsonNode.end()) return; for (auto & i : jsonNode["inputs"].items()) { if (i.value().is_array()) { // FIXME: remove, obsolete - InputPath path; + InputAttrPath path; for (auto & j : i.value()) path.push_back(j); node.inputs.insert_or_assign(i.key(), path); @@ -197,10 +212,12 @@ std::pair LockFile::toJSON() const /* For backward compatibility, omit the "__final" attribute. We never allow non-final inputs in lock files anyway. */ - assert(lockedNode->lockedRef.input.isFinal()); + assert(lockedNode->lockedRef.input.isFinal() || lockedNode->lockedRef.input.isRelative()); n["locked"].erase("__final"); if (!lockedNode->isFlake) n["flake"] = false; + if (lockedNode->parentInputAttrPath) + n["parent"] = *lockedNode->parentInputAttrPath; } nodes[key] = std::move(n); @@ -228,7 +245,7 @@ std::ostream & operator <<(std::ostream & stream, const LockFile & lockFile) return stream; } -std::optional LockFile::isUnlocked() const +std::optional LockFile::isUnlocked(const fetchers::Settings & fetchSettings) const { std::set> nodes; @@ -244,10 +261,22 @@ std::optional LockFile::isUnlocked() const visit(root); + /* Return whether the input is either locked, or, if + `allow-dirty-locks` is enabled, it has a NAR hash. In the + latter case, we can verify the input but we may not be able to + fetch it from anywhere. */ + auto isConsideredLocked = [&](const fetchers::Input & input) + { + return input.isLocked() || (fetchSettings.allowDirtyLocks && input.getNarHash()); + }; + for (auto & i : nodes) { if (i == ref(root)) continue; auto node = i.dynamic_pointer_cast(); - if (node && (!node->lockedRef.input.isLocked() || !node->lockedRef.input.isFinal())) + if (node + && (!isConsideredLocked(node->lockedRef.input) + || !node->lockedRef.input.isFinal()) + && !node->lockedRef.input.isRelative()) return node->lockedRef; } @@ -260,36 +289,36 @@ bool LockFile::operator ==(const LockFile & other) const return toJSON().first == other.toJSON().first; } -InputPath parseInputPath(std::string_view s) +InputAttrPath parseInputAttrPath(std::string_view s) { - InputPath path; + InputAttrPath path; for (auto & elem : tokenizeString>(s, "/")) { if (!std::regex_match(elem, flakeIdRegex)) - throw UsageError("invalid flake input path element '%s'", elem); + throw UsageError("invalid flake input attribute path element '%s'", elem); path.push_back(elem); } return path; } -std::map LockFile::getAllInputs() const +std::map LockFile::getAllInputs() const { std::set> done; - std::map res; + std::map res; - std::function node)> recurse; + std::function node)> recurse; - recurse = [&](const InputPath & prefix, ref node) + recurse = [&](const InputAttrPath & prefix, ref node) { if (!done.insert(node).second) return; for (auto &[id, input] : node->inputs) { - auto inputPath(prefix); - inputPath.push_back(id); - res.emplace(inputPath, input); + auto inputAttrPath(prefix); + inputAttrPath.push_back(id); + res.emplace(inputAttrPath, input); if (auto child = std::get_if<0>(&input)) - recurse(inputPath, *child); + recurse(inputAttrPath, *child); } }; @@ -313,7 +342,7 @@ std::ostream & operator <<(std::ostream & stream, const Node::Edge & edge) if (auto node = std::get_if<0>(&edge)) stream << describe((*node)->lockedRef); else if (auto follows = std::get_if<1>(&edge)) - stream << fmt("follows '%s'", printInputPath(*follows)); + stream << fmt("follows '%s'", printInputAttrPath(*follows)); return stream; } @@ -340,15 +369,15 @@ std::string LockFile::diff(const LockFile & oldLocks, const LockFile & newLocks) while (i != oldFlat.end() || j != newFlat.end()) { if (j != newFlat.end() && (i == oldFlat.end() || i->first > j->first)) { res += fmt("• " ANSI_GREEN "Added input '%s':" ANSI_NORMAL "\n %s\n", - printInputPath(j->first), j->second); + printInputAttrPath(j->first), j->second); ++j; } else if (i != oldFlat.end() && (j == newFlat.end() || i->first < j->first)) { - res += fmt("• " ANSI_RED "Removed input '%s'" ANSI_NORMAL "\n", printInputPath(i->first)); + res += fmt("• " ANSI_RED "Removed input '%s'" ANSI_NORMAL "\n", printInputAttrPath(i->first)); ++i; } else { if (!equals(i->second, j->second)) { res += fmt("• " ANSI_BOLD "Updated input '%s':" ANSI_NORMAL "\n %s\n → %s\n", - printInputPath(i->first), + printInputAttrPath(i->first), i->second, j->second); } @@ -364,19 +393,19 @@ void LockFile::check() { auto inputs = getAllInputs(); - for (auto & [inputPath, input] : inputs) { + for (auto & [inputAttrPath, input] : inputs) { if (auto follows = std::get_if<1>(&input)) { if (!follows->empty() && !findInput(*follows)) throw Error("input '%s' follows a non-existent input '%s'", - printInputPath(inputPath), - printInputPath(*follows)); + printInputAttrPath(inputAttrPath), + printInputAttrPath(*follows)); } } } void check(); -std::string printInputPath(const InputPath & path) +std::string printInputAttrPath(const InputAttrPath & path) { return concatStringsSep("/", path); } diff --git a/src/libflake/meson.build b/src/libflake/meson.build index 2c1a70a1840..bc8533e1518 100644 --- a/src/libflake/meson.build +++ b/src/libflake/meson.build @@ -4,8 +4,6 @@ project('nix-flake', 'cpp', 'cpp_std=c++2a', # TODO(Qyriad): increase the warning level 'warning_level=1', - 'debug=true', - 'optimization=2', 'errorlogs=true', # Please print logs for tests that fail ], meson_version : '>= 1.1', @@ -14,7 +12,7 @@ project('nix-flake', 'cpp', cxx = meson.get_compiler('cpp') -subdir('build-utils-meson/deps-lists') +subdir('nix-meson-build-support/deps-lists') deps_private_maybe_subproject = [ ] @@ -24,52 +22,50 @@ deps_public_maybe_subproject = [ dependency('nix-fetchers'), dependency('nix-expr'), ] -subdir('build-utils-meson/subprojects') +subdir('nix-meson-build-support/subprojects') nlohmann_json = dependency('nlohmann_json', version : '>= 3.9') deps_public += nlohmann_json -add_project_arguments( - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. - '-include', 'config-util.hh', - '-include', 'config-store.hh', - # '-include', 'config-fetchers.h', - '-include', 'config-expr.hh', - language : 'cpp', -) +subdir('nix-meson-build-support/common') + +subdir('nix-meson-build-support/generate-header') -subdir('build-utils-meson/common') +generated_headers = [] +foreach header : [ + 'call-flake.nix', +] + generated_headers += gen_header.process(header) +endforeach sources = files( - 'flake/config.cc', - 'flake/flake.cc', - 'flake/flakeref.cc', - 'flake/lockfile.cc', - 'flake/settings.cc', - 'flake/url-name.cc', + 'config.cc', + 'flake.cc', + 'flakeref.cc', + 'lockfile.cc', + 'flake-primops.cc', + 'settings.cc', + 'url-name.cc', ) -include_dirs = [include_directories('.')] +subdir('include/nix/flake') -headers = files( - 'flake/flake.hh', - 'flake/flakeref.hh', - 'flake/lockfile.hh', - 'flake/settings.hh', - 'flake/url-name.hh', -) +subdir('nix-meson-build-support/export-all-symbols') +subdir('nix-meson-build-support/windows-version') this_library = library( 'nixflake', sources, + generated_headers, dependencies : deps_public + deps_private + deps_other, + include_directories : include_dirs, + link_args: linker_export_flags, prelink : true, # For C++ static initializers install : true, ) -install_headers(headers, subdir : 'nix', preserve_path : true) +install_headers(headers, subdir : 'nix/flake', preserve_path : true) libraries_private = [] -subdir('build-utils-meson/export') +subdir('nix-meson-build-support/export') diff --git a/src/libflake/nix-meson-build-support b/src/libflake/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/libflake/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/libflake/package.nix b/src/libflake/package.nix index fff481720d9..dd442a44ec9 100644 --- a/src/libflake/package.nix +++ b/src/libflake/package.nix @@ -1,16 +1,16 @@ -{ lib -, stdenv -, mkMesonLibrary +{ + lib, + mkMesonLibrary, -, nix-util -, nix-store -, nix-fetchers -, nix-expr -, nlohmann_json + nix-util, + nix-store, + nix-fetchers, + nix-expr, + nlohmann_json, -# Configuration Options + # Configuration Options -, version + version, }: let @@ -23,11 +23,13 @@ mkMesonLibrary (finalAttrs: { workDir = ./.; fileset = fileset.unions [ - ../../build-utils-meson - ./build-utils-meson + ../../nix-meson-build-support + ./nix-meson-build-support ../../.version ./.version ./meson.build + ./include/nix/flake/meson.build + ./call-flake.nix (fileset.fileFilter (file: file.hasExt "cc") ./.) (fileset.fileFilter (file: file.hasExt "hh") ./.) ]; @@ -40,18 +42,6 @@ mkMesonLibrary (finalAttrs: { nlohmann_json ]; - preConfigure = - # "Inline" .version so it's not a symlink, and includes the suffix. - # Do the meson utils, without modification. - '' - chmod u+w ./.version - echo ${version} > ../../.version - ''; - - env = lib.optionalAttrs (stdenv.isLinux && !(stdenv.hostPlatform.isStatic && stdenv.system == "aarch64-linux")) { - LDFLAGS = "-fuse-ld=gold"; - }; - meta = { platforms = lib.platforms.unix ++ lib.platforms.windows; }; diff --git a/src/libflake/settings.cc b/src/libflake/settings.cc new file mode 100644 index 00000000000..bab7f9439db --- /dev/null +++ b/src/libflake/settings.cc @@ -0,0 +1,15 @@ +#include "nix/flake/settings.hh" +#include "nix/flake/flake-primops.hh" + +namespace nix::flake { + +Settings::Settings() {} + +void Settings::configureEvalSettings(nix::EvalSettings & evalSettings) const +{ + evalSettings.extraPrimOps.emplace_back(primops::getFlake(*this)); + evalSettings.extraPrimOps.emplace_back(primops::parseFlakeRef); + evalSettings.extraPrimOps.emplace_back(primops::flakeRefToString); +} + +} // namespace nix diff --git a/src/libflake/flake/url-name.cc b/src/libflake/url-name.cc similarity index 98% rename from src/libflake/flake/url-name.cc rename to src/libflake/url-name.cc index d62b345522a..3e3311cf740 100644 --- a/src/libflake/flake/url-name.cc +++ b/src/libflake/url-name.cc @@ -1,4 +1,4 @@ -#include "url-name.hh" +#include "nix/flake/url-name.hh" #include #include diff --git a/src/libmain-c/build-utils-meson b/src/libmain-c/build-utils-meson deleted file mode 120000 index 5fff21bab55..00000000000 --- a/src/libmain-c/build-utils-meson +++ /dev/null @@ -1 +0,0 @@ -../../build-utils-meson \ No newline at end of file diff --git a/src/libmain-c/meson.build b/src/libmain-c/meson.build index 3cb1e4baae7..e420520e6b1 100644 --- a/src/libmain-c/meson.build +++ b/src/libmain-c/meson.build @@ -4,8 +4,6 @@ project('nix-main-c', 'cpp', 'cpp_std=c++2a', # TODO(Qyriad): increase the warning level 'warning_level=1', - 'debug=true', - 'optimization=2', 'errorlogs=true', # Please print logs for tests that fail ], meson_version : '>= 1.1', @@ -14,9 +12,7 @@ project('nix-main-c', 'cpp', cxx = meson.get_compiler('cpp') -subdir('build-utils-meson/deps-lists') - -configdata = configuration_data() +subdir('nix-meson-build-support/deps-lists') deps_private_maybe_subproject = [ dependency('nix-util'), @@ -27,33 +23,9 @@ deps_public_maybe_subproject = [ dependency('nix-util-c'), dependency('nix-store-c'), ] -subdir('build-utils-meson/subprojects') - -# TODO rename, because it will conflict with downstream projects -configdata.set_quoted('PACKAGE_VERSION', meson.project_version()) - -config_h = configure_file( - configuration : configdata, - output : 'config-main.h', -) - -add_project_arguments( - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. - - # From C++ libraries, only for internals - '-include', 'config-util.hh', - '-include', 'config-store.hh', - '-include', 'config-main.hh', - - # From C libraries, for our public, installed headers too - '-include', 'config-util.h', - '-include', 'config-store.h', - '-include', 'config-main.h', - language : 'cpp', -) +subdir('nix-meson-build-support/subprojects') -subdir('build-utils-meson/common') +subdir('nix-meson-build-support/common') sources = files( 'nix_api_main.cc', @@ -61,12 +33,12 @@ sources = files( include_dirs = [include_directories('.')] -headers = [config_h] + files( +headers = files( 'nix_api_main.h', ) -subdir('build-utils-meson/export-all-symbols') -subdir('build-utils-meson/windows-version') +subdir('nix-meson-build-support/export-all-symbols') +subdir('nix-meson-build-support/windows-version') this_library = library( 'nixmainc', @@ -78,8 +50,8 @@ this_library = library( install : true, ) -install_headers(headers, subdir : 'nix', preserve_path : true) +install_headers(headers, preserve_path : true) libraries_private = [] -subdir('build-utils-meson/export') +subdir('nix-meson-build-support/export') diff --git a/src/libmain-c/nix-meson-build-support b/src/libmain-c/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/libmain-c/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/libmain-c/nix_api_main.cc b/src/libmain-c/nix_api_main.cc index 692d53f47e0..eacb804554c 100644 --- a/src/libmain-c/nix_api_main.cc +++ b/src/libmain-c/nix_api_main.cc @@ -3,7 +3,7 @@ #include "nix_api_util.h" #include "nix_api_util_internal.h" -#include "plugin.hh" +#include "nix/main/plugin.hh" nix_err nix_init_plugins(nix_c_context * context) { diff --git a/src/libmain-c/package.nix b/src/libmain-c/package.nix index 5522037f3ec..f019a917d36 100644 --- a/src/libmain-c/package.nix +++ b/src/libmain-c/package.nix @@ -1,15 +1,15 @@ -{ lib -, stdenv -, mkMesonLibrary +{ + lib, + mkMesonLibrary, -, nix-util-c -, nix-store -, nix-store-c -, nix-main + nix-util-c, + nix-store, + nix-store-c, + nix-main, -# Configuration Options + # Configuration Options -, version + version, }: let @@ -22,8 +22,8 @@ mkMesonLibrary (finalAttrs: { workDir = ./.; fileset = fileset.unions [ - ../../build-utils-meson - ./build-utils-meson + ../../nix-meson-build-support + ./nix-meson-build-support ../../.version ./.version ./meson.build @@ -40,21 +40,9 @@ mkMesonLibrary (finalAttrs: { nix-main ]; - preConfigure = - # "Inline" .version so it's not a symlink, and includes the suffix. - # Do the meson utils, without modification. - '' - chmod u+w ./.version - echo ${version} > ../../.version - ''; - mesonFlags = [ ]; - env = lib.optionalAttrs (stdenv.isLinux && !(stdenv.hostPlatform.isStatic && stdenv.system == "aarch64-linux")) { - LDFLAGS = "-fuse-ld=gold"; - }; - meta = { platforms = lib.platforms.unix ++ lib.platforms.windows; }; diff --git a/src/libmain/build-utils-meson b/src/libmain/build-utils-meson deleted file mode 120000 index 5fff21bab55..00000000000 --- a/src/libmain/build-utils-meson +++ /dev/null @@ -1 +0,0 @@ -../../build-utils-meson \ No newline at end of file diff --git a/src/libmain/common-args.cc b/src/libmain/common-args.cc index 13d358623cc..dcf252a4f3a 100644 --- a/src/libmain/common-args.cc +++ b/src/libmain/common-args.cc @@ -1,11 +1,13 @@ -#include "common-args.hh" -#include "args/root.hh" -#include "config-global.hh" -#include "globals.hh" -#include "logging.hh" -#include "loggers.hh" -#include "util.hh" -#include "plugin.hh" +#include + +#include "nix/main/common-args.hh" +#include "nix/util/args/root.hh" +#include "nix/util/config-global.hh" +#include "nix/store/globals.hh" +#include "nix/util/logging.hh" +#include "nix/main/loggers.hh" +#include "nix/util/util.hh" +#include "nix/main/plugin.hh" namespace nix { @@ -57,7 +59,7 @@ MixCommonArgs::MixCommonArgs(const std::string & programName) if (hasPrefix(s.first, prefix)) completions.add(s.first, fmt("Set the `%s` setting.", s.first)); } - } + }, }); addFlag({ @@ -75,7 +77,7 @@ MixCommonArgs::MixCommonArgs(const std::string & programName) .labels = Strings{"jobs"}, .handler = {[=](std::string s) { settings.set("max-jobs", s); - }} + }}, }); std::string cat = "Options to override configuration settings"; @@ -93,5 +95,18 @@ void MixCommonArgs::initialFlagsProcessed() pluginsInited(); } - +template +void MixPrintJSON::printJSON(const T /* nlohmann::json */ & json) +{ + auto suspension = logger->suspend(); + if (outputPretty) { + logger->writeToStdout(json.dump(2)); + } else { + logger->writeToStdout(json.dump()); + } } + +template void MixPrintJSON::printJSON(const nlohmann::json & json); + + +} // namespace nix diff --git a/src/libmain/common-args.hh b/src/libmain/common-args.hh deleted file mode 100644 index c35406c3bcc..00000000000 --- a/src/libmain/common-args.hh +++ /dev/null @@ -1,70 +0,0 @@ -#pragma once -///@file - -#include "args.hh" -#include "repair-flag.hh" - -namespace nix { - -//static constexpr auto commonArgsCategory = "Miscellaneous common options"; -static constexpr auto loggingCategory = "Logging-related options"; -static constexpr auto miscCategory = "Miscellaneous global options"; - -class MixCommonArgs : public virtual Args -{ - void initialFlagsProcessed() override; -public: - std::string programName; - MixCommonArgs(const std::string & programName); -protected: - virtual void pluginsInited() {} -}; - -struct MixDryRun : virtual Args -{ - bool dryRun = false; - - MixDryRun() - { - addFlag({ - .longName = "dry-run", - .description = "Show what this command would do without doing it.", - //.category = commonArgsCategory, - .handler = {&dryRun, true}, - }); - } -}; - -struct MixJSON : virtual Args -{ - bool json = false; - - MixJSON() - { - addFlag({ - .longName = "json", - .description = "Produce output in JSON format, suitable for consumption by another program.", - //.category = commonArgsCategory, - .handler = {&json, true}, - }); - } -}; - -struct MixRepair : virtual Args -{ - RepairFlag repair = NoRepair; - - MixRepair() - { - addFlag({ - .longName = "repair", - .description = - "During evaluation, rewrite missing or corrupted files in the Nix store. " - "During building, rebuild missing or corrupted store paths.", - .category = miscCategory, - .handler = {&repair, Repair}, - }); - } -}; - -} diff --git a/src/libmain/include/nix/main/common-args.hh b/src/libmain/include/nix/main/common-args.hh new file mode 100644 index 00000000000..cc6d3d3f0c6 --- /dev/null +++ b/src/libmain/include/nix/main/common-args.hh @@ -0,0 +1,125 @@ +#pragma once +///@file + +#include "nix/util/args.hh" +#include "nix/util/repair-flag.hh" + +namespace nix { + +//static constexpr auto commonArgsCategory = "Miscellaneous common options"; +static constexpr auto loggingCategory = "Logging-related options"; +static constexpr auto miscCategory = "Miscellaneous global options"; + +class MixCommonArgs : public virtual Args +{ + void initialFlagsProcessed() override; +public: + std::string programName; + MixCommonArgs(const std::string & programName); +protected: + virtual void pluginsInited() {} +}; + +struct MixDryRun : virtual Args +{ + bool dryRun = false; + + MixDryRun() + { + addFlag({ + .longName = "dry-run", + .description = "Show what this command would do without doing it.", + .handler = {&dryRun, true}, + }); + } +}; + +/** + * Commands that can print JSON according to the + * `--pretty`/`--no-pretty` flag. + * + * This is distinct from MixJSON, because for some commands, + * JSON outputs is not optional. + */ +struct MixPrintJSON : virtual Args +{ + bool outputPretty = isatty(STDOUT_FILENO); + + MixPrintJSON() + { + addFlag({ + .longName = "pretty", + .description = + R"( + Print multi-line, indented JSON output for readability. + + Default: indent if output is to a terminal. + + This option is only effective when `--json` is also specified. + )", + .handler = {&outputPretty, true}, + }); + addFlag({ + .longName = "no-pretty", + .description = + R"( + Print compact JSON output on a single line, even when the output is a terminal. + Some commands may print multiple JSON objects on separate lines. + + See `--pretty`. + )", + .handler = {&outputPretty, false}, + }); + }; + + /** + * Print an `nlohmann::json` to stdout + * + * - respecting `--pretty` / `--no-pretty`. + * - suspending the progress bar + * + * This is a template to avoid accidental coercions from `string` to `json` in the caller, + * to avoid mistakenly passing an already serialized JSON to this function. + * + * It is not recommended to print a JSON string - see the JSON guidelines + * about extensibility, https://nix.dev/manual/nix/development/development/json-guideline.html - + * but you _can_ print a sole JSON string by explicitly coercing it to + * `nlohmann::json` first. + */ + template >> + void printJSON(const T & json); +}; + +/** Optional JSON support via `--json` flag */ +struct MixJSON : virtual Args, virtual MixPrintJSON +{ + bool json = false; + + MixJSON() + { + addFlag({ + .longName = "json", + .description = "Produce output in JSON format, suitable for consumption by another program.", + .handler = {&json, true}, + }); + } +}; + +struct MixRepair : virtual Args +{ + RepairFlag repair = NoRepair; + + MixRepair() + { + addFlag({ + .longName = "repair", + .description = + "During evaluation, rewrite missing or corrupted files in the Nix store. " + "During building, rebuild missing or corrupted store paths.", + .category = miscCategory, + .handler = {&repair, Repair}, + }); + } +}; + +} diff --git a/src/libmain/loggers.hh b/src/libmain/include/nix/main/loggers.hh similarity index 82% rename from src/libmain/loggers.hh rename to src/libmain/include/nix/main/loggers.hh index e5721420cb6..061b4a32afe 100644 --- a/src/libmain/loggers.hh +++ b/src/libmain/include/nix/main/loggers.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "types.hh" +#include "nix/util/types.hh" namespace nix { @@ -16,6 +16,4 @@ enum class LogFormat { void setLogFormat(const std::string & logFormatStr); void setLogFormat(const LogFormat & logFormat); -void createDefaultLogger(); - } diff --git a/src/libmain/include/nix/main/meson.build b/src/libmain/include/nix/main/meson.build new file mode 100644 index 00000000000..992a5ff0ece --- /dev/null +++ b/src/libmain/include/nix/main/meson.build @@ -0,0 +1,11 @@ +# Public headers directory + +include_dirs = [include_directories('../..')] + +headers = files( + 'common-args.hh', + 'loggers.hh', + 'plugin.hh', + 'progress-bar.hh', + 'shared.hh', +) diff --git a/src/libmain/plugin.hh b/src/libmain/include/nix/main/plugin.hh similarity index 100% rename from src/libmain/plugin.hh rename to src/libmain/include/nix/main/plugin.hh diff --git a/src/libmain/include/nix/main/progress-bar.hh b/src/libmain/include/nix/main/progress-bar.hh new file mode 100644 index 00000000000..f49fb2198c9 --- /dev/null +++ b/src/libmain/include/nix/main/progress-bar.hh @@ -0,0 +1,10 @@ +#pragma once +///@file + +#include "nix/util/logging.hh" + +namespace nix { + +std::unique_ptr makeProgressBar(); + +} diff --git a/src/libmain/shared.hh b/src/libmain/include/nix/main/shared.hh similarity index 87% rename from src/libmain/shared.hh rename to src/libmain/include/nix/main/shared.hh index 712b404d3e1..4d4b816e714 100644 --- a/src/libmain/shared.hh +++ b/src/libmain/include/nix/main/shared.hh @@ -1,13 +1,13 @@ #pragma once ///@file -#include "file-descriptor.hh" -#include "processes.hh" -#include "args.hh" -#include "args/root.hh" -#include "common-args.hh" -#include "path.hh" -#include "derived-path.hh" +#include "nix/util/file-descriptor.hh" +#include "nix/util/processes.hh" +#include "nix/util/args.hh" +#include "nix/util/args/root.hh" +#include "nix/main/common-args.hh" +#include "nix/store/path.hh" +#include "nix/store/derived-path.hh" #include @@ -35,15 +35,17 @@ void printVersion(const std::string & programName); void printGCWarning(); class Store; +struct MissingPaths; void printMissing( ref store, const std::vector & paths, Verbosity lvl = lvlInfo); -void printMissing(ref store, const StorePathSet & willBuild, - const StorePathSet & willSubstitute, const StorePathSet & unknown, - uint64_t downloadSize, uint64_t narSize, Verbosity lvl = lvlInfo); +void printMissing( + ref store, + const MissingPaths & missing, + Verbosity lvl = lvlInfo); std::string getArg(const std::string & opt, Strings::iterator & i, const Strings::iterator & end); @@ -70,11 +72,6 @@ struct LegacyArgs : public MixCommonArgs, public RootArgs }; -/** - * Show the manual page for the specified program. - */ -void showManPage(const std::string & name); - /** * The constructor of this class starts a pager if standard output is a * terminal and $PAGER is set. Standard output is redirected to the diff --git a/src/libmain/loggers.cc b/src/libmain/loggers.cc index a4e0530c8f9..c78e49b6326 100644 --- a/src/libmain/loggers.cc +++ b/src/libmain/loggers.cc @@ -1,12 +1,13 @@ -#include "loggers.hh" -#include "environment-variables.hh" -#include "progress-bar.hh" +#include "nix/main/loggers.hh" +#include "nix/util/environment-variables.hh" +#include "nix/main/progress-bar.hh" namespace nix { LogFormat defaultLogFormat = LogFormat::raw; -LogFormat parseLogFormat(const std::string & logFormatStr) { +LogFormat parseLogFormat(const std::string & logFormatStr) +{ if (logFormatStr == "raw" || getEnv("NIX_GET_COMPLETIONS")) return LogFormat::raw; else if (logFormatStr == "raw-with-logs") @@ -20,14 +21,15 @@ LogFormat parseLogFormat(const std::string & logFormatStr) { throw Error("option 'log-format' has an invalid value '%s'", logFormatStr); } -Logger * makeDefaultLogger() { +std::unique_ptr makeDefaultLogger() +{ switch (defaultLogFormat) { case LogFormat::raw: return makeSimpleLogger(false); case LogFormat::rawWithLogs: return makeSimpleLogger(true); case LogFormat::internalJSON: - return makeJSONLogger(*makeSimpleLogger(true)); + return makeJSONLogger(getStandardError()); case LogFormat::bar: return makeProgressBar(); case LogFormat::barWithLogs: { @@ -40,16 +42,14 @@ Logger * makeDefaultLogger() { } } -void setLogFormat(const std::string & logFormatStr) { +void setLogFormat(const std::string & logFormatStr) +{ setLogFormat(parseLogFormat(logFormatStr)); } -void setLogFormat(const LogFormat & logFormat) { +void setLogFormat(const LogFormat & logFormat) +{ defaultLogFormat = logFormat; - createDefaultLogger(); -} - -void createDefaultLogger() { logger = makeDefaultLogger(); } diff --git a/src/libmain/meson.build b/src/libmain/meson.build index 6c6298e2be7..65fcb6239a2 100644 --- a/src/libmain/meson.build +++ b/src/libmain/meson.build @@ -4,8 +4,6 @@ project('nix-main', 'cpp', 'cpp_std=c++2a', # TODO(Qyriad): increase the warning level 'warning_level=1', - 'debug=true', - 'optimization=2', 'errorlogs=true', # Please print logs for tests that fail ], meson_version : '>= 1.1', @@ -14,7 +12,7 @@ project('nix-main', 'cpp', cxx = meson.get_compiler('cpp') -subdir('build-utils-meson/deps-lists') +subdir('nix-meson-build-support/deps-lists') configdata = configuration_data() @@ -23,8 +21,12 @@ deps_private_maybe_subproject = [ deps_public_maybe_subproject = [ dependency('nix-util'), dependency('nix-store'), + # FIXME: This is only here for the NIX_USE_BOEHMGC macro dependency + # Removing nix-expr will make the build more concurrent and is + # architecturally nice, perhaps. + dependency('nix-expr'), ] -subdir('build-utils-meson/subprojects') +subdir('nix-meson-build-support/subprojects') pubsetbuf_test = ''' #include @@ -44,21 +46,12 @@ configdata.set( description: 'Optionally used for buffering on standard error' ) -config_h = configure_file( +config_priv_h = configure_file( configuration : configdata, - output : 'config-main.hh', + output : 'main-config-private.hh', ) -add_project_arguments( - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. - '-include', 'config-util.hh', - '-include', 'config-store.hh', - '-include', 'config-main.hh', - language : 'cpp', -) - -subdir('build-utils-meson/common') +subdir('nix-meson-build-support/common') sources = files( 'common-args.cc', @@ -74,26 +67,24 @@ if host_machine.system() != 'windows' ) endif -include_dirs = [include_directories('.')] +subdir('include/nix/main') -headers = [config_h] + files( - 'common-args.hh', - 'loggers.hh', - 'plugin.hh', - 'progress-bar.hh', - 'shared.hh', -) +subdir('nix-meson-build-support/export-all-symbols') +subdir('nix-meson-build-support/windows-version') this_library = library( 'nixmain', sources, + config_priv_h, dependencies : deps_public + deps_private + deps_other, + include_directories : include_dirs, + link_args: linker_export_flags, prelink : true, # For C++ static initializers install : true, ) -install_headers(headers, subdir : 'nix', preserve_path : true) +install_headers(headers, subdir : 'nix/main', preserve_path : true) libraries_private = [] -subdir('build-utils-meson/export') +subdir('nix-meson-build-support/export') diff --git a/src/libmain/nix-meson-build-support b/src/libmain/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/libmain/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/libmain/package.nix b/src/libmain/package.nix index 7e7b8047284..7b0a4dee7da 100644 --- a/src/libmain/package.nix +++ b/src/libmain/package.nix @@ -1,15 +1,16 @@ -{ lib -, stdenv -, mkMesonLibrary +{ + lib, + mkMesonLibrary, -, openssl + openssl, -, nix-util -, nix-store + nix-util, + nix-store, + nix-expr, -# Configuration Options + # Configuration Options -, version + version, }: let @@ -22,33 +23,26 @@ mkMesonLibrary (finalAttrs: { workDir = ./.; fileset = fileset.unions [ - ../../build-utils-meson - ./build-utils-meson + ../../nix-meson-build-support + ./nix-meson-build-support ../../.version ./.version ./meson.build + ./include/nix/main/meson.build (fileset.fileFilter (file: file.hasExt "cc") ./.) (fileset.fileFilter (file: file.hasExt "hh") ./.) ]; propagatedBuildInputs = [ + # FIXME: This is only here for the NIX_USE_BOEHMGC macro dependency + # Removing nix-expr will make the build more concurrent and is + # architecturally nice, perhaps. + nix-expr nix-util nix-store openssl ]; - preConfigure = - # "Inline" .version so it's not a symlink, and includes the suffix. - # Do the meson utils, without modification. - '' - chmod u+w ./.version - echo ${version} > ../../.version - ''; - - env = lib.optionalAttrs (stdenv.isLinux && !(stdenv.hostPlatform.isStatic && stdenv.system == "aarch64-linux")) { - LDFLAGS = "-fuse-ld=gold"; - }; - meta = { platforms = lib.platforms.unix ++ lib.platforms.windows; }; diff --git a/src/libmain/plugin.cc b/src/libmain/plugin.cc index ccfd7f9003a..760a096ad21 100644 --- a/src/libmain/plugin.cc +++ b/src/libmain/plugin.cc @@ -4,8 +4,9 @@ #include -#include "config-global.hh" -#include "signals.hh" +#include "nix/util/config-global.hh" +#include "nix/util/signals.hh" +#include "nix/util/file-system.hh" namespace nix { @@ -18,7 +19,7 @@ struct PluginFilesSetting : public BaseSetting const Paths & def, const std::string & name, const std::string & description, - const std::set & aliases = {}) + const StringSet & aliases = {}) : BaseSetting(def, true, name, description, aliases) { options->addSetting(this); @@ -42,9 +43,9 @@ struct PluginSettings : Config {}, "plugin-files", R"( - A list of plugin files to be loaded by Nix. Each of these files will - be dlopened by Nix. If they contain the symbol `nix_plugin_entry()`, - this symbol will be called. Alternatively, they can affect execution + A list of plugin files to be loaded by Nix. Each of these files is + dlopened by Nix. If they contain the symbol `nix_plugin_entry()`, + this symbol is called. Alternatively, they can affect execution through static initialization. In particular, these plugins may construct static instances of RegisterPrimOp to add new primops or constants to the expression language, RegisterStoreImplementation to add new store @@ -59,7 +60,7 @@ struct PluginSettings : Config itself, they must be DSOs compatible with the instance of Nix running at the time (i.e. compiled against the same headers, not linked to any incompatible libraries). They should not be linked to - any Nix libs directly, as those will be available already at load + any Nix libraries directly, as those are already available at load time. If an entry in the list is a directory, all files in the directory @@ -77,13 +78,13 @@ void initPlugins() for (const auto & pluginFile : pluginSettings.pluginFiles.get()) { std::vector pluginFiles; try { - auto ents = std::filesystem::directory_iterator{pluginFile}; + auto ents = DirectoryIterator{pluginFile}; for (const auto & ent : ents) { checkInterrupt(); pluginFiles.emplace_back(ent.path()); } - } catch (std::filesystem::filesystem_error & e) { - if (e.code() != std::errc::not_a_directory) + } catch (SysError & e) { + if (e.errNo != ENOTDIR) throw; pluginFiles.emplace_back(pluginFile); } diff --git a/src/libmain/progress-bar.cc b/src/libmain/progress-bar.cc index fa0b73ebef3..173ab876c2a 100644 --- a/src/libmain/progress-bar.cc +++ b/src/libmain/progress-bar.cc @@ -1,8 +1,8 @@ -#include "progress-bar.hh" -#include "terminal.hh" -#include "sync.hh" -#include "store-api.hh" -#include "names.hh" +#include "nix/main/progress-bar.hh" +#include "nix/util/terminal.hh" +#include "nix/util/sync.hh" +#include "nix/store/store-api.hh" +#include "nix/store/names.hh" #include #include @@ -73,8 +73,13 @@ class ProgressBar : public Logger uint64_t corruptedPaths = 0, untrustedPaths = 0; bool active = true; - bool paused = false; + size_t suspensions = 0; bool haveUpdate = true; + + bool isPaused() const + { + return suspensions > 0; + } }; /** Helps avoid unnecessary redraws, see `redraw()` */ @@ -117,29 +122,43 @@ class ProgressBar : public Logger { { auto state(state_.lock()); - if (!state->active) return; - state->active = false; - writeToStderr("\r\e[K"); - updateCV.notify_one(); - quitCV.notify_one(); + if (state->active) { + state->active = false; + writeToStderr("\r\e[K"); + updateCV.notify_one(); + quitCV.notify_one(); + } } - updateThread.join(); + if (updateThread.joinable()) + updateThread.join(); } void pause() override { auto state (state_.lock()); - state->paused = true; + state->suspensions++; + if (state->suspensions > 1) { + // already paused + return; + } + if (state->active) writeToStderr("\r\e[K"); } void resume() override { auto state (state_.lock()); - state->paused = false; - if (state->active) - writeToStderr("\r\e[K"); - state->haveUpdate = true; - updateCV.notify_one(); + if (state->suspensions == 0) { + log(lvlError, "nix::ProgressBar: resume() called without a matching preceding pause(). This is a bug."); + return; + } else { + state->suspensions--; + } + if (state->suspensions == 0) { + if (state->active) + writeToStderr("\r\e[K"); + state->haveUpdate = true; + updateCV.notify_one(); + } } bool isVerbose() override @@ -240,7 +259,7 @@ class ProgressBar : public Logger update(*state); } - /* Check whether an activity has an ancestore with the specified + /* Check whether an activity has an ancestor with the specified type. */ bool hasAncestor(State & state, ActivityType type, ActivityId act) { @@ -287,23 +306,21 @@ class ProgressBar : public Logger else if (type == resBuildLogLine || type == resPostBuildLogLine) { auto lastLine = chomp(getS(fields, 0)); - if (!lastLine.empty()) { - auto i = state->its.find(act); - assert(i != state->its.end()); - ActInfo info = *i->second; - if (printBuildLogs) { - auto suffix = "> "; - if (type == resPostBuildLogLine) { - suffix = " (post)> "; - } - log(*state, lvlInfo, ANSI_FAINT + info.name.value_or("unnamed") + suffix + ANSI_NORMAL + lastLine); - } else { - state->activities.erase(i->second); - info.lastLine = lastLine; - state->activities.emplace_back(info); - i->second = std::prev(state->activities.end()); - update(*state); + auto i = state->its.find(act); + assert(i != state->its.end()); + ActInfo info = *i->second; + if (printBuildLogs) { + auto suffix = "> "; + if (type == resPostBuildLogLine) { + suffix = " (post)> "; } + log(*state, lvlInfo, ANSI_FAINT + info.name.value_or("unnamed") + suffix + ANSI_NORMAL + lastLine); + } else { + state->activities.erase(i->second); + info.lastLine = lastLine; + state->activities.emplace_back(info); + i->second = std::prev(state->activities.end()); + update(*state); } } @@ -365,7 +382,7 @@ class ProgressBar : public Logger /** * Redraw, if the output has changed. * - * Excessive redrawing is noticable on slow terminals, and it interferes + * Excessive redrawing is noticeable on slow terminals, and it interferes * with text selection in some terminals, including libvte-based terminal * emulators. */ @@ -383,7 +400,7 @@ class ProgressBar : public Logger auto nextWakeup = std::chrono::milliseconds::max(); state.haveUpdate = false; - if (state.paused || !state.active) return nextWakeup; + if (state.isPaused() || !state.active) return nextWakeup; std::string line; @@ -555,21 +572,9 @@ class ProgressBar : public Logger } }; -Logger * makeProgressBar() -{ - return new ProgressBar(isTTY()); -} - -void startProgressBar() -{ - logger = makeProgressBar(); -} - -void stopProgressBar() +std::unique_ptr makeProgressBar() { - auto progressBar = dynamic_cast(logger); - if (progressBar) progressBar->stop(); - + return std::make_unique(isTTY()); } } diff --git a/src/libmain/progress-bar.hh b/src/libmain/progress-bar.hh deleted file mode 100644 index c3c6e383334..00000000000 --- a/src/libmain/progress-bar.hh +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once -///@file - -#include "logging.hh" - -namespace nix { - -Logger * makeProgressBar(); - -void startProgressBar(); - -void stopProgressBar(); - -} diff --git a/src/libmain/shared.cc b/src/libmain/shared.cc index 50f90bfb314..0982810d1a7 100644 --- a/src/libmain/shared.cc +++ b/src/libmain/shared.cc @@ -1,11 +1,11 @@ -#include "globals.hh" -#include "current-process.hh" -#include "shared.hh" -#include "store-api.hh" -#include "gc-store.hh" -#include "loggers.hh" -#include "progress-bar.hh" -#include "signals.hh" +#include "nix/store/globals.hh" +#include "nix/util/current-process.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-api.hh" +#include "nix/store/gc-store.hh" +#include "nix/main/loggers.hh" +#include "nix/main/progress-bar.hh" +#include "nix/util/signals.hh" #include #include @@ -22,8 +22,11 @@ #include -#include "exit.hh" -#include "strings.hh" +#include "nix/util/exit.hh" +#include "nix/util/strings.hh" + +#include "main-config-private.hh" +#include "nix/expr/config.hh" namespace nix { @@ -43,43 +46,41 @@ void printGCWarning() void printMissing(ref store, const std::vector & paths, Verbosity lvl) { - uint64_t downloadSize, narSize; - StorePathSet willBuild, willSubstitute, unknown; - store->queryMissing(paths, willBuild, willSubstitute, unknown, downloadSize, narSize); - printMissing(store, willBuild, willSubstitute, unknown, downloadSize, narSize, lvl); + printMissing(store, store->queryMissing(paths), lvl); } -void printMissing(ref store, const StorePathSet & willBuild, - const StorePathSet & willSubstitute, const StorePathSet & unknown, - uint64_t downloadSize, uint64_t narSize, Verbosity lvl) +void printMissing( + ref store, + const MissingPaths & missing, + Verbosity lvl) { - if (!willBuild.empty()) { - if (willBuild.size() == 1) + if (!missing.willBuild.empty()) { + if (missing.willBuild.size() == 1) printMsg(lvl, "this derivation will be built:"); else - printMsg(lvl, "these %d derivations will be built:", willBuild.size()); - auto sorted = store->topoSortPaths(willBuild); + printMsg(lvl, "these %d derivations will be built:", missing.willBuild.size()); + auto sorted = store->topoSortPaths(missing.willBuild); reverse(sorted.begin(), sorted.end()); for (auto & i : sorted) printMsg(lvl, " %s", store->printStorePath(i)); } - if (!willSubstitute.empty()) { - const float downloadSizeMiB = downloadSize / (1024.f * 1024.f); - const float narSizeMiB = narSize / (1024.f * 1024.f); - if (willSubstitute.size() == 1) { + if (!missing.willSubstitute.empty()) { + const float downloadSizeMiB = missing.downloadSize / (1024.f * 1024.f); + const float narSizeMiB = missing.narSize / (1024.f * 1024.f); + if (missing.willSubstitute.size() == 1) { printMsg(lvl, "this path will be fetched (%.2f MiB download, %.2f MiB unpacked):", downloadSizeMiB, narSizeMiB); } else { printMsg(lvl, "these %d paths will be fetched (%.2f MiB download, %.2f MiB unpacked):", - willSubstitute.size(), + missing.willSubstitute.size(), downloadSizeMiB, narSizeMiB); } std::vector willSubstituteSorted = {}; - std::for_each(willSubstitute.begin(), willSubstitute.end(), + std::for_each(missing.willSubstitute.begin(), missing.willSubstitute.end(), [&](const StorePath &p) { willSubstituteSorted.push_back(&p); }); std::sort(willSubstituteSorted.begin(), willSubstituteSorted.end(), [](const StorePath *lhs, const StorePath *rhs) { @@ -92,10 +93,10 @@ void printMissing(ref store, const StorePathSet & willBuild, printMsg(lvl, " %s", store->printStorePath(*p)); } - if (!unknown.empty()) { + if (!missing.unknown.empty()) { printMsg(lvl, "don't know how to build these paths%s:", (settings.readOnlyMode ? " (may be caused by read-only store access)" : "")); - for (auto & i : unknown) + for (auto & i : missing.unknown) printMsg(lvl, " %s", store->printStorePath(i)); } } @@ -141,7 +142,7 @@ void initNix(bool loadConfig) if (sigaction(SIGUSR1, &act, 0)) throw SysError("handling SIGUSR1"); #endif -#if __APPLE__ +#ifdef __APPLE__ /* HACK: on darwin, we need can’t use sigprocmask with SIGWINCH. * Instead, add a dummy sigaction handler, and signalHandlerThread * can handle the rest. */ @@ -173,16 +174,6 @@ void initNix(bool loadConfig) now. In particular, store objects should be readable by everybody. */ umask(0022); - - /* Initialise the PRNG. */ - struct timeval tv; - gettimeofday(&tv, 0); -#ifndef _WIN32 - srandom(tv.tv_usec); -#endif - srand(tv.tv_usec); - - } @@ -228,7 +219,7 @@ LegacyArgs::LegacyArgs(const std::string & programName, .handler = {[=](std::string s) { auto n = string2IntWithUnitPrefix(s); settings.set(dest, std::to_string(n)); - }} + }}, }); }; @@ -297,7 +288,7 @@ void printVersion(const std::string & programName) std::cout << fmt("%1% (Nix) %2%", programName, nixVersion) << std::endl; if (verbosity > lvlInfo) { Strings cfg; -#if HAVE_BOEHMGC +#if NIX_USE_BOEHMGC cfg.push_back("gc"); #endif cfg.push_back("signed-caches"); @@ -315,20 +306,6 @@ void printVersion(const std::string & programName) throw Exit(); } - -void showManPage(const std::string & name) -{ - restoreProcessContext(); - setEnv("MANPATH", settings.nixManDir.c_str()); - execlp("man", "man", name.c_str(), nullptr); - if (errno == ENOENT) { - // Not SysError because we don't want to suffix the errno, aka No such file or directory. - throw Error("The '%1%' command was not found, but it is needed for '%2%' and some other '%3%' commands' help text. Perhaps you could install the '%1%' command?", "man", name.c_str(), "nix-*"); - } - throw SysError("command 'man %1%' failed", name.c_str()); -} - - int handleExceptions(const std::string & programName, std::function fun) { ReceiveInterrupts receiveInterrupts; // FIXME: need better place for this @@ -338,29 +315,34 @@ int handleExceptions(const std::string & programName, std::function fun) std::string error = ANSI_RED "error:" ANSI_NORMAL " "; try { try { - fun(); - } catch (...) { - /* Subtle: we have to make sure that any `interrupted' - condition is discharged before we reach printMsg() - below, since otherwise it will throw an (uncaught) - exception. */ - setInterruptThrown(); - throw; + try { + fun(); + } catch (...) { + /* Subtle: we have to make sure that any `interrupted' + condition is discharged before we reach printMsg() + below, since otherwise it will throw an (uncaught) + exception. */ + setInterruptThrown(); + throw; + } + } catch (Exit & e) { + return e.status; + } catch (UsageError & e) { + logError(e.info()); + printError("Try '%1% --help' for more information.", programName); + return 1; + } catch (BaseError & e) { + logError(e.info()); + return e.info().status; + } catch (std::bad_alloc & e) { + printError(error + "out of memory"); + return 1; + } catch (std::exception & e) { + printError(error + e.what()); + return 1; } - } catch (Exit & e) { - return e.status; - } catch (UsageError & e) { - logError(e.info()); - printError("Try '%1% --help' for more information.", programName); - return 1; - } catch (BaseError & e) { - logError(e.info()); - return e.info().status; - } catch (std::bad_alloc & e) { - printError(error + "out of memory"); - return 1; - } catch (std::exception & e) { - printError(error + e.what()); + } catch (...) { + /* In case logger also throws just give up. */ return 1; } @@ -375,7 +357,7 @@ RunPager::RunPager() if (!pager) pager = getenv("PAGER"); if (pager && ((std::string) pager == "" || (std::string) pager == "cat")) return; - stopProgressBar(); + logger->stop(); Pipe toPager; toPager.create(); diff --git a/src/libmain/unix/stack.cc b/src/libmain/unix/stack.cc index 10f71c1dcad..cee21d2a21c 100644 --- a/src/libmain/unix/stack.cc +++ b/src/libmain/unix/stack.cc @@ -1,5 +1,5 @@ -#include "error.hh" -#include "shared.hh" +#include "nix/util/error.hh" +#include "nix/main/shared.hh" #include #include diff --git a/src/libstore-c/build-utils-meson b/src/libstore-c/build-utils-meson deleted file mode 120000 index 5fff21bab55..00000000000 --- a/src/libstore-c/build-utils-meson +++ /dev/null @@ -1 +0,0 @@ -../../build-utils-meson \ No newline at end of file diff --git a/src/libstore-c/meson.build b/src/libstore-c/meson.build index 44b5fe11d81..eb556316107 100644 --- a/src/libstore-c/meson.build +++ b/src/libstore-c/meson.build @@ -4,8 +4,6 @@ project('nix-store-c', 'cpp', 'cpp_std=c++2a', # TODO(Qyriad): increase the warning level 'warning_level=1', - 'debug=true', - 'optimization=2', 'errorlogs=true', # Please print logs for tests that fail ], meson_version : '>= 1.1', @@ -14,9 +12,7 @@ project('nix-store-c', 'cpp', cxx = meson.get_compiler('cpp') -subdir('build-utils-meson/deps-lists') - -configdata = configuration_data() +subdir('nix-meson-build-support/deps-lists') deps_private_maybe_subproject = [ dependency('nix-util'), @@ -25,31 +21,9 @@ deps_private_maybe_subproject = [ deps_public_maybe_subproject = [ dependency('nix-util-c'), ] -subdir('build-utils-meson/subprojects') - -# TODO rename, because it will conflict with downstream projects -configdata.set_quoted('PACKAGE_VERSION', meson.project_version()) - -config_h = configure_file( - configuration : configdata, - output : 'config-store.h', -) - -add_project_arguments( - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. - - # From C++ libraries, only for internals - '-include', 'config-util.hh', - '-include', 'config-store.hh', - - # From C libraries, for our public, installed headers too - '-include', 'config-util.h', - '-include', 'config-store.h', - language : 'cpp', -) +subdir('nix-meson-build-support/subprojects') -subdir('build-utils-meson/common') +subdir('nix-meson-build-support/common') sources = files( 'nix_api_store.cc', @@ -57,15 +31,15 @@ sources = files( include_dirs = [include_directories('.')] -headers = [config_h] + files( +headers = files( 'nix_api_store.h', ) # TODO don't install this once tests don't use it and/or move the header into `libstore`, non-`c` headers += files('nix_api_store_internal.h') -subdir('build-utils-meson/export-all-symbols') -subdir('build-utils-meson/windows-version') +subdir('nix-meson-build-support/export-all-symbols') +subdir('nix-meson-build-support/windows-version') this_library = library( 'nixstorec', @@ -77,8 +51,8 @@ this_library = library( install : true, ) -install_headers(headers, subdir : 'nix', preserve_path : true) +install_headers(headers, preserve_path : true) libraries_private = [] -subdir('build-utils-meson/export') +subdir('nix-meson-build-support/export') diff --git a/src/libstore-c/nix-meson-build-support b/src/libstore-c/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/libstore-c/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/libstore-c/nix_api_store.cc b/src/libstore-c/nix_api_store.cc index fb7391276c4..b7b437e9c81 100644 --- a/src/libstore-c/nix_api_store.cc +++ b/src/libstore-c/nix_api_store.cc @@ -3,11 +3,12 @@ #include "nix_api_util.h" #include "nix_api_util_internal.h" -#include "path.hh" -#include "store-api.hh" -#include "build-result.hh" +#include "nix/store/path.hh" +#include "nix/store/store-api.hh" +#include "nix/store/store-open.hh" +#include "nix/store/build-result.hh" -#include "globals.hh" +#include "nix/store/globals.hh" nix_err nix_libstore_init(nix_c_context * context) { @@ -42,7 +43,7 @@ Store * nix_store_open(nix_c_context * context, const char * uri, const char *** if (!params) return new Store{nix::openStore(uri_str)}; - nix::Store::Params params_map; + nix::Store::Config::Params params_map; for (size_t i = 0; params[i] != nullptr; i++) { params_map[params[i][0]] = params[i][1]; } @@ -67,6 +68,17 @@ nix_err nix_store_get_uri(nix_c_context * context, Store * store, nix_get_string NIXC_CATCH_ERRS } +nix_err +nix_store_get_storedir(nix_c_context * context, Store * store, nix_get_string_callback callback, void * user_data) +{ + if (context) + context->last_err_code = NIX_OK; + try { + return call_nix_get_string_callback(store->ptr->storeDir, callback, user_data); + } + NIXC_CATCH_ERRS +} + nix_err nix_store_get_version(nix_c_context * context, Store * store, nix_get_string_callback callback, void * user_data) { @@ -89,6 +101,18 @@ bool nix_store_is_valid_path(nix_c_context * context, Store * store, StorePath * NIXC_CATCH_ERRS_RES(false); } +nix_err nix_store_real_path( + nix_c_context * context, Store * store, StorePath * path, nix_get_string_callback callback, void * user_data) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto res = store->ptr->toRealPath(path->path); + return call_nix_get_string_callback(res, callback, user_data); + } + NIXC_CATCH_ERRS +} + StorePath * nix_store_parse_path(nix_c_context * context, Store * store, const char * path) { if (context) diff --git a/src/libstore-c/nix_api_store.h b/src/libstore-c/nix_api_store.h index 282ccc28514..e55bc3f59ff 100644 --- a/src/libstore-c/nix_api_store.h +++ b/src/libstore-c/nix_api_store.h @@ -48,12 +48,27 @@ nix_err nix_libstore_init_no_load_config(nix_c_context * context); * Store instances may share state and resources behind the scenes. * * @param[out] context Optional, stores error information - * @param[in] uri URI of the Nix store, copied. See [*Store URL format* in the Nix Reference + * + * @param[in] uri @parblock + * URI of the Nix store, copied. + * + * If `NULL`, the store from the settings will be used. + * Note that `"auto"` holds a strange middle ground, reading part of the general environment, but not all of it. It + * ignores `NIX_REMOTE` and the `store` option. For this reason, `NULL` is most likely the better choice. + * + * For supported store URLs, see [*Store URL format* in the Nix Reference * Manual](https://nixos.org/manual/nix/stable/store/types/#store-url-format). - * @param[in] params optional, null-terminated array of key-value pairs, e.g. {{"endpoint", - * "https://s3.local"}}. See [*Store Types* in the Nix Reference - * Manual](https://nixos.org/manual/nix/stable/store/types). + * @endparblock + * + * @param[in] params @parblock + * optional, null-terminated array of key-value pairs, e.g. {{"endpoint", + * "https://s3.local"}}. + * + * See [*Store Types* in the Nix Reference Manual](https://nixos.org/manual/nix/stable/store/types). + * @endparblock + * * @return a Store pointer, NULL in case of errors + * * @see nix_store_free */ Store * nix_store_open(nix_c_context * context, const char * uri, const char *** params); @@ -78,7 +93,18 @@ void nix_store_free(Store * store); */ nix_err nix_store_get_uri(nix_c_context * context, Store * store, nix_get_string_callback callback, void * user_data); -// returns: owned StorePath* +/** + * @brief get the storeDir of a Nix store, typically `"/nix/store"` + * @param[out] context Optional, stores error information + * @param[in] store nix store reference + * @param[in] callback Called with the URI. + * @param[in] user_data optional, arbitrary data, passed to the callback when it's called. + * @see nix_get_string_callback + * @return error code, NIX_OK on success. + */ +nix_err +nix_store_get_storedir(nix_c_context * context, Store * store, nix_get_string_callback callback, void * user_data); + /** * @brief Parse a Nix store path into a StorePath * @@ -123,6 +149,26 @@ void nix_store_path_free(StorePath * p); * @return true or false, error info in context */ bool nix_store_is_valid_path(nix_c_context * context, Store * store, StorePath * path); + +/** + * @brief Get the physical location of a store path + * + * A store may reside at a different location than its `storeDir` suggests. + * This situation is called a relocated store. + * Relocated stores are used during NixOS installation, as well as in restricted computing environments that don't offer + * a writable `/nix/store`. + * + * Not all types of stores support this operation. + * + * @param[in] context Optional, stores error information + * @param[in] store nix store reference + * @param[in] path the path to get the real path from + * @param[in] callback called with the real path + * @param[in] user_data arbitrary data, passed to the callback when it's called. + */ +nix_err nix_store_real_path( + nix_c_context * context, Store * store, StorePath * path, nix_get_string_callback callback, void * user_data); + // nix_err nix_store_ensure(Store*, const char*); // nix_err nix_store_build_paths(Store*); /** diff --git a/src/libstore-c/nix_api_store_internal.h b/src/libstore-c/nix_api_store_internal.h index 13db0c07cf8..b0194bfd3ad 100644 --- a/src/libstore-c/nix_api_store_internal.h +++ b/src/libstore-c/nix_api_store_internal.h @@ -1,6 +1,6 @@ #ifndef NIX_API_STORE_INTERNAL_H #define NIX_API_STORE_INTERNAL_H -#include "store-api.hh" +#include "nix/store/store-api.hh" struct Store { diff --git a/src/libstore-c/package.nix b/src/libstore-c/package.nix index 896a1a39f05..fde17c78e01 100644 --- a/src/libstore-c/package.nix +++ b/src/libstore-c/package.nix @@ -1,13 +1,13 @@ -{ lib -, stdenv -, mkMesonLibrary +{ + lib, + mkMesonLibrary, -, nix-util-c -, nix-store + nix-util-c, + nix-store, -# Configuration Options + # Configuration Options -, version + version, }: let @@ -20,8 +20,8 @@ mkMesonLibrary (finalAttrs: { workDir = ./.; fileset = fileset.unions [ - ../../build-utils-meson - ./build-utils-meson + ../../nix-meson-build-support + ./nix-meson-build-support ../../.version ./.version ./meson.build @@ -36,21 +36,9 @@ mkMesonLibrary (finalAttrs: { nix-store ]; - preConfigure = - # "Inline" .version so it's not a symlink, and includes the suffix. - # Do the meson utils, without modification. - '' - chmod u+w ./.version - echo ${version} > ../../.version - ''; - mesonFlags = [ ]; - env = lib.optionalAttrs (stdenv.isLinux && !(stdenv.hostPlatform.isStatic && stdenv.system == "aarch64-linux")) { - LDFLAGS = "-fuse-ld=gold"; - }; - meta = { platforms = lib.platforms.unix ++ lib.platforms.windows; }; diff --git a/src/libstore-test-support/build-utils-meson b/src/libstore-test-support/build-utils-meson deleted file mode 120000 index 5fff21bab55..00000000000 --- a/src/libstore-test-support/build-utils-meson +++ /dev/null @@ -1 +0,0 @@ -../../build-utils-meson \ No newline at end of file diff --git a/src/libstore-test-support/derived-path.cc b/src/libstore-test-support/derived-path.cc new file mode 100644 index 00000000000..c7714449c03 --- /dev/null +++ b/src/libstore-test-support/derived-path.cc @@ -0,0 +1,71 @@ +#include + +#include + +#include "nix/store/tests/derived-path.hh" + +namespace rc { +using namespace nix; + +Gen Arbitrary::arbitrary() +{ + return gen::map(gen::arbitrary(), [](StorePath path) { + return DerivedPath::Opaque{ + .path = path, + }; + }); +} + +Gen Arbitrary::arbitrary() +{ + return gen::mapcat(gen::arbitrary(), [](SingleDerivedPath drvPath) { + return gen::map(gen::arbitrary(), [drvPath](StorePathName outputPath) { + return SingleDerivedPath::Built{ + .drvPath = make_ref(drvPath), + .output = outputPath.name, + }; + }); + }); +} + +Gen Arbitrary::arbitrary() +{ + return gen::mapcat(gen::arbitrary(), [](SingleDerivedPath drvPath) { + return gen::map(gen::arbitrary(), [drvPath](OutputsSpec outputs) { + return DerivedPath::Built{ + .drvPath = make_ref(drvPath), + .outputs = outputs, + }; + }); + }); +} + +Gen Arbitrary::arbitrary() +{ + return gen::mapcat(gen::inRange(0, std::variant_size_v), [](uint8_t n) { + switch (n) { + case 0: + return gen::map(gen::arbitrary(), [](SingleDerivedPath a) { return a; }); + case 1: + return gen::map(gen::arbitrary(), [](SingleDerivedPath a) { return a; }); + default: + assert(false); + } + }); +} + +Gen Arbitrary::arbitrary() +{ + return gen::mapcat(gen::inRange(0, std::variant_size_v), [](uint8_t n) { + switch (n) { + case 0: + return gen::map(gen::arbitrary(), [](DerivedPath a) { return a; }); + case 1: + return gen::map(gen::arbitrary(), [](DerivedPath a) { return a; }); + default: + assert(false); + } + }); +} + +} diff --git a/src/libstore-test-support/tests/derived-path.hh b/src/libstore-test-support/include/nix/store/tests/derived-path.hh similarity index 84% rename from src/libstore-test-support/tests/derived-path.hh rename to src/libstore-test-support/include/nix/store/tests/derived-path.hh index 98d61f2283d..642ce557ce8 100644 --- a/src/libstore-test-support/tests/derived-path.hh +++ b/src/libstore-test-support/include/nix/store/tests/derived-path.hh @@ -3,10 +3,10 @@ #include -#include +#include "nix/store/derived-path.hh" -#include "tests/path.hh" -#include "tests/outputs-spec.hh" +#include "nix/store/tests/path.hh" +#include "nix/store/tests/outputs-spec.hh" namespace rc { using namespace nix; diff --git a/src/libstore-test-support/tests/libstore.hh b/src/libstore-test-support/include/nix/store/tests/libstore.hh similarity index 58% rename from src/libstore-test-support/tests/libstore.hh rename to src/libstore-test-support/include/nix/store/tests/libstore.hh index 84be52c230b..822ec3aa854 100644 --- a/src/libstore-test-support/tests/libstore.hh +++ b/src/libstore-test-support/include/nix/store/tests/libstore.hh @@ -4,7 +4,8 @@ #include #include -#include "store-api.hh" +#include "nix/store/store-api.hh" +#include "nix/store/store-open.hh" namespace nix { @@ -19,12 +20,12 @@ public: protected: LibStoreTest() : store(openStore({ - .variant = - StoreReference::Specified{ - .scheme = "dummy", - }, - .params = {}, - })) + .variant = + StoreReference::Specified{ + .scheme = "dummy", + }, + .params = {}, + })) { } diff --git a/src/libstore-test-support/include/nix/store/tests/meson.build b/src/libstore-test-support/include/nix/store/tests/meson.build new file mode 100644 index 00000000000..ae5db049e0a --- /dev/null +++ b/src/libstore-test-support/include/nix/store/tests/meson.build @@ -0,0 +1,12 @@ +# Public headers directory + +include_dirs = [include_directories('../../..')] + +headers = files( + 'derived-path.hh', + 'libstore.hh', + 'nix_api_store.hh', + 'outputs-spec.hh', + 'path.hh', + 'protocol.hh', +) diff --git a/src/libstore-test-support/tests/nix_api_store.hh b/src/libstore-test-support/include/nix/store/tests/nix_api_store.hh similarity index 71% rename from src/libstore-test-support/tests/nix_api_store.hh rename to src/libstore-test-support/include/nix/store/tests/nix_api_store.hh index b7d5c2c33f7..e51be3dab5a 100644 --- a/src/libstore-test-support/tests/nix_api_store.hh +++ b/src/libstore-test-support/include/nix/store/tests/nix_api_store.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "tests/nix_api_util.hh" +#include "nix/util/tests/nix_api_util.hh" -#include "file-system.hh" +#include "nix/util/file-system.hh" #include #include "nix_api_store.h" @@ -11,8 +11,6 @@ #include #include -namespace fs { using namespace std::filesystem; } - namespace nixC { class nix_api_store_test : public nix_api_util_context { @@ -27,15 +25,17 @@ public: { nix_store_free(store); - for (auto & path : fs::recursive_directory_iterator(nixDir)) { - fs::permissions(path, fs::perms::owner_all); + for (auto & path : std::filesystem::recursive_directory_iterator(nixDir)) { + std::filesystem::permissions(path, std::filesystem::perms::owner_all); } - fs::remove_all(nixDir); + std::filesystem::remove_all(nixDir); } Store * store; std::string nixDir; std::string nixStoreDir; + std::string nixStateDir; + std::string nixLogDir; protected: void init_local_store() @@ -45,7 +45,7 @@ protected: auto tmpl = nix::defaultTempDir() + "/tests_nix-store."; for (size_t i = 0; true; ++i) { nixDir = tmpl + std::string { i }; - if (fs::create_directory(nixDir)) break; + if (std::filesystem::create_directory(nixDir)) break; } #else // resolve any symlinks in i.e. on macOS /tmp -> /private/tmp @@ -55,11 +55,13 @@ protected: #endif nixStoreDir = nixDir + "/my_nix_store"; + nixStateDir = nixDir + "/my_state"; + nixLogDir = nixDir + "/my_log"; // Options documented in `nix help-stores` const char * p1[] = {"store", nixStoreDir.c_str()}; - const char * p2[] = {"state", (new std::string(nixDir + "/my_state"))->c_str()}; - const char * p3[] = {"log", (new std::string(nixDir + "/my_log"))->c_str()}; + const char * p2[] = {"state", nixStateDir.c_str()}; + const char * p3[] = {"log", nixLogDir.c_str()}; const char ** params[] = {p1, p2, p3, nullptr}; diff --git a/src/libstore-test-support/tests/outputs-spec.hh b/src/libstore-test-support/include/nix/store/tests/outputs-spec.hh similarity index 72% rename from src/libstore-test-support/tests/outputs-spec.hh rename to src/libstore-test-support/include/nix/store/tests/outputs-spec.hh index f5bf9042d20..c13c992b6f8 100644 --- a/src/libstore-test-support/tests/outputs-spec.hh +++ b/src/libstore-test-support/include/nix/store/tests/outputs-spec.hh @@ -3,9 +3,9 @@ #include -#include +#include "nix/store/outputs-spec.hh" -#include "tests/path.hh" +#include "nix/store/tests/path.hh" namespace rc { using namespace nix; diff --git a/src/libstore-test-support/tests/path.hh b/src/libstore-test-support/include/nix/store/tests/path.hh similarity index 93% rename from src/libstore-test-support/tests/path.hh rename to src/libstore-test-support/include/nix/store/tests/path.hh index 4751b3373a3..59ff604d7ca 100644 --- a/src/libstore-test-support/tests/path.hh +++ b/src/libstore-test-support/include/nix/store/tests/path.hh @@ -3,7 +3,7 @@ #include -#include +#include "nix/store/path.hh" namespace nix { diff --git a/src/libstore-test-support/tests/protocol.hh b/src/libstore-test-support/include/nix/store/tests/protocol.hh similarity index 96% rename from src/libstore-test-support/tests/protocol.hh rename to src/libstore-test-support/include/nix/store/tests/protocol.hh index 3f6799d1ccb..acd10bf9d8c 100644 --- a/src/libstore-test-support/tests/protocol.hh +++ b/src/libstore-test-support/include/nix/store/tests/protocol.hh @@ -4,8 +4,8 @@ #include #include -#include "tests/libstore.hh" -#include "tests/characterization.hh" +#include "nix/store/tests/libstore.hh" +#include "nix/util/tests/characterization.hh" namespace nix { diff --git a/src/libstore-test-support/meson.build b/src/libstore-test-support/meson.build index 1f230914f6c..779b122fa29 100644 --- a/src/libstore-test-support/meson.build +++ b/src/libstore-test-support/meson.build @@ -4,8 +4,6 @@ project('nix-store-test-support', 'cpp', 'cpp_std=c++2a', # TODO(Qyriad): increase the warning level 'warning_level=1', - 'debug=true', - 'optimization=2', 'errorlogs=true', # Please print logs for tests that fail ], meson_version : '>= 1.1', @@ -14,7 +12,7 @@ project('nix-store-test-support', 'cpp', cxx = meson.get_compiler('cpp') -subdir('build-utils-meson/deps-lists') +subdir('nix-meson-build-support/deps-lists') deps_private_maybe_subproject = [ ] @@ -24,40 +22,23 @@ deps_public_maybe_subproject = [ dependency('nix-store'), dependency('nix-store-c'), ] -subdir('build-utils-meson/subprojects') +subdir('nix-meson-build-support/subprojects') rapidcheck = dependency('rapidcheck') deps_public += rapidcheck -add_project_arguments( - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. - '-include', 'config-util.hh', - '-include', 'config-store.hh', - language : 'cpp', -) - -subdir('build-utils-meson/common') +subdir('nix-meson-build-support/common') sources = files( - 'tests/derived-path.cc', - 'tests/outputs-spec.cc', - 'tests/path.cc', + 'derived-path.cc', + 'outputs-spec.cc', + 'path.cc', ) -include_dirs = [include_directories('.')] - -headers = files( - 'tests/derived-path.hh', - 'tests/libstore.hh', - 'tests/nix_api_store.hh', - 'tests/outputs-spec.hh', - 'tests/path.hh', - 'tests/protocol.hh', -) +subdir('include/nix/store/tests') -subdir('build-utils-meson/export-all-symbols') -subdir('build-utils-meson/windows-version') +subdir('nix-meson-build-support/export-all-symbols') +subdir('nix-meson-build-support/windows-version') this_library = library( 'nix-store-test-support', @@ -71,8 +52,8 @@ this_library = library( install : true, ) -install_headers(headers, subdir : 'nix', preserve_path : true) +install_headers(headers, subdir : 'nix/store/tests', preserve_path : true) libraries_private = [] -subdir('build-utils-meson/export') +subdir('nix-meson-build-support/export') diff --git a/src/libstore-test-support/nix-meson-build-support b/src/libstore-test-support/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/libstore-test-support/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/libstore-test-support/outputs-spec.cc b/src/libstore-test-support/outputs-spec.cc new file mode 100644 index 00000000000..5b5251361d4 --- /dev/null +++ b/src/libstore-test-support/outputs-spec.cc @@ -0,0 +1,27 @@ +#include "nix/store/tests/outputs-spec.hh" + +#include + +namespace rc { +using namespace nix; + +Gen Arbitrary::arbitrary() +{ + return gen::mapcat( + gen::inRange(0, std::variant_size_v), [](uint8_t n) -> Gen { + switch (n) { + case 0: + return gen::just((OutputsSpec) OutputsSpec::All{}); + case 1: + return gen::map( + gen::nonEmpty( + gen::container( + gen::map(gen::arbitrary(), [](StorePathName n) { return n.name; }))), + [](StringSet names) { return (OutputsSpec) OutputsSpec::Names{names}; }); + default: + assert(false); + } + }); +} + +} diff --git a/src/libstore-test-support/package.nix b/src/libstore-test-support/package.nix index 2543049fe73..391ddeefda2 100644 --- a/src/libstore-test-support/package.nix +++ b/src/libstore-test-support/package.nix @@ -1,16 +1,16 @@ -{ lib -, stdenv -, mkMesonLibrary +{ + lib, + mkMesonLibrary, -, nix-util-test-support -, nix-store -, nix-store-c + nix-util-test-support, + nix-store, + nix-store-c, -, rapidcheck + rapidcheck, -# Configuration Options + # Configuration Options -, version + version, }: let @@ -23,12 +23,13 @@ mkMesonLibrary (finalAttrs: { workDir = ./.; fileset = fileset.unions [ - ../../build-utils-meson - ./build-utils-meson + ../../nix-meson-build-support + ./nix-meson-build-support ../../.version ./.version ./meson.build # ./meson.options + ./include/nix/store/tests/meson.build (fileset.fileFilter (file: file.hasExt "cc") ./.) (fileset.fileFilter (file: file.hasExt "hh") ./.) ]; @@ -40,21 +41,9 @@ mkMesonLibrary (finalAttrs: { rapidcheck ]; - preConfigure = - # "Inline" .version so it's not a symlink, and includes the suffix. - # Do the meson utils, without modification. - '' - chmod u+w ./.version - echo ${version} > ../../.version - ''; - mesonFlags = [ ]; - env = lib.optionalAttrs (stdenv.isLinux && !(stdenv.hostPlatform.isStatic && stdenv.system == "aarch64-linux")) { - LDFLAGS = "-fuse-ld=gold"; - }; - meta = { platforms = lib.platforms.unix ++ lib.platforms.windows; }; diff --git a/src/libstore-test-support/tests/path.cc b/src/libstore-test-support/path.cc similarity index 92% rename from src/libstore-test-support/tests/path.cc rename to src/libstore-test-support/path.cc index 8ddda80277c..47c1d693b7d 100644 --- a/src/libstore-test-support/tests/path.cc +++ b/src/libstore-test-support/path.cc @@ -3,11 +3,11 @@ #include -#include "path-regex.hh" -#include "store-api.hh" +#include "nix/store/path-regex.hh" +#include "nix/store/store-api.hh" -#include "tests/hash.hh" -#include "tests/path.hh" +#include "nix/util/tests/hash.hh" +#include "nix/store/tests/path.hh" namespace nix { diff --git a/src/libstore-test-support/tests/derived-path.cc b/src/libstore-test-support/tests/derived-path.cc deleted file mode 100644 index 078615bbd01..00000000000 --- a/src/libstore-test-support/tests/derived-path.cc +++ /dev/null @@ -1,57 +0,0 @@ -#include - -#include - -#include "tests/derived-path.hh" - -namespace rc { -using namespace nix; - -Gen Arbitrary::arbitrary() -{ - return gen::just(DerivedPath::Opaque { - .path = *gen::arbitrary(), - }); -} - -Gen Arbitrary::arbitrary() -{ - return gen::just(SingleDerivedPath::Built { - .drvPath = make_ref(*gen::arbitrary()), - .output = (*gen::arbitrary()).name, - }); -} - -Gen Arbitrary::arbitrary() -{ - return gen::just(DerivedPath::Built { - .drvPath = make_ref(*gen::arbitrary()), - .outputs = *gen::arbitrary(), - }); -} - -Gen Arbitrary::arbitrary() -{ - switch (*gen::inRange(0, std::variant_size_v)) { - case 0: - return gen::just(*gen::arbitrary()); - case 1: - return gen::just(*gen::arbitrary()); - default: - assert(false); - } -} - -Gen Arbitrary::arbitrary() -{ - switch (*gen::inRange(0, std::variant_size_v)) { - case 0: - return gen::just(*gen::arbitrary()); - case 1: - return gen::just(*gen::arbitrary()); - default: - assert(false); - } -} - -} diff --git a/src/libstore-test-support/tests/outputs-spec.cc b/src/libstore-test-support/tests/outputs-spec.cc deleted file mode 100644 index e9d6022037b..00000000000 --- a/src/libstore-test-support/tests/outputs-spec.cc +++ /dev/null @@ -1,24 +0,0 @@ -#include "tests/outputs-spec.hh" - -#include - -namespace rc { -using namespace nix; - -Gen Arbitrary::arbitrary() -{ - switch (*gen::inRange(0, std::variant_size_v)) { - case 0: - return gen::just((OutputsSpec) OutputsSpec::All { }); - case 1: - return gen::just((OutputsSpec) OutputsSpec::Names { - *gen::nonEmpty(gen::container(gen::map( - gen::arbitrary(), - [](StorePathName n) { return n.name; }))), - }); - default: - assert(false); - } -} - -} diff --git a/src/libstore-tests/build-utils-meson b/src/libstore-tests/build-utils-meson deleted file mode 120000 index 5fff21bab55..00000000000 --- a/src/libstore-tests/build-utils-meson +++ /dev/null @@ -1 +0,0 @@ -../../build-utils-meson \ No newline at end of file diff --git a/src/libstore-tests/common-protocol.cc b/src/libstore-tests/common-protocol.cc index c8f6dd002d5..5164f154abf 100644 --- a/src/libstore-tests/common-protocol.cc +++ b/src/libstore-tests/common-protocol.cc @@ -3,11 +3,11 @@ #include #include -#include "common-protocol.hh" -#include "common-protocol-impl.hh" -#include "build-result.hh" -#include "tests/protocol.hh" -#include "tests/characterization.hh" +#include "nix/store/common-protocol.hh" +#include "nix/store/common-protocol-impl.hh" +#include "nix/store/build-result.hh" +#include "nix/store/tests/protocol.hh" +#include "nix/util/tests/characterization.hh" namespace nix { @@ -154,7 +154,7 @@ CHARACTERIZATION_TEST( CHARACTERIZATION_TEST( set, "set", - (std::tuple, std::set, std::set, std::set>> { + (std::tuple> { { }, { "" }, { "", "foo", "bar" }, diff --git a/src/libstore-tests/content-address.cc b/src/libstore-tests/content-address.cc index 72eb84fec11..c208c944d50 100644 --- a/src/libstore-tests/content-address.cc +++ b/src/libstore-tests/content-address.cc @@ -1,6 +1,6 @@ #include -#include "content-address.hh" +#include "nix/store/content-address.hh" namespace nix { diff --git a/src/libstore-tests/data/derivation/advanced-attributes-defaults.drv b/src/libstore-tests/data/derivation/advanced-attributes-defaults.drv deleted file mode 120000 index f8f30ac321c..00000000000 --- a/src/libstore-tests/data/derivation/advanced-attributes-defaults.drv +++ /dev/null @@ -1 +0,0 @@ -../../../../tests/functional/derivation/advanced-attributes-defaults.drv \ No newline at end of file diff --git a/src/libstore-tests/data/derivation/advanced-attributes-structured-attrs-defaults.drv b/src/libstore-tests/data/derivation/advanced-attributes-structured-attrs-defaults.drv deleted file mode 120000 index 837e9a0e437..00000000000 --- a/src/libstore-tests/data/derivation/advanced-attributes-structured-attrs-defaults.drv +++ /dev/null @@ -1 +0,0 @@ -../../../../tests/functional/derivation/advanced-attributes-structured-attrs-defaults.drv \ No newline at end of file diff --git a/src/libstore-tests/data/derivation/advanced-attributes-structured-attrs.drv b/src/libstore-tests/data/derivation/advanced-attributes-structured-attrs.drv deleted file mode 120000 index e08bb573791..00000000000 --- a/src/libstore-tests/data/derivation/advanced-attributes-structured-attrs.drv +++ /dev/null @@ -1 +0,0 @@ -../../../../tests/functional/derivation/advanced-attributes-structured-attrs.drv \ No newline at end of file diff --git a/src/libstore-tests/data/derivation/advanced-attributes-structured-attrs.json b/src/libstore-tests/data/derivation/advanced-attributes-structured-attrs.json deleted file mode 100644 index 32442812467..00000000000 --- a/src/libstore-tests/data/derivation/advanced-attributes-structured-attrs.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "args": [ - "-c", - "echo hello > $out" - ], - "builder": "/bin/bash", - "env": { - "__json": "{\"__darwinAllowLocalNetworking\":true,\"__impureHostDeps\":[\"/usr/bin/ditto\"],\"__noChroot\":true,\"__sandboxProfile\":\"sandcastle\",\"allowSubstitutes\":false,\"builder\":\"/bin/bash\",\"impureEnvVars\":[\"UNICORN\"],\"name\":\"advanced-attributes-structured-attrs\",\"outputChecks\":{\"bin\":{\"disallowedReferences\":[\"/nix/store/7rhsm8i393hm1wcsmph782awg1hi2f7x-bar\"],\"disallowedRequisites\":[\"/nix/store/7rhsm8i393hm1wcsmph782awg1hi2f7x-bar\"]},\"dev\":{\"maxClosureSize\":5909,\"maxSize\":789},\"out\":{\"allowedReferences\":[\"/nix/store/3c08bzb71z4wiag719ipjxr277653ynp-foo\"],\"allowedRequisites\":[\"/nix/store/3c08bzb71z4wiag719ipjxr277653ynp-foo\"]}},\"outputs\":[\"out\",\"bin\",\"dev\"],\"preferLocalBuild\":true,\"requiredSystemFeatures\":[\"rainbow\",\"uid-range\"],\"system\":\"my-system\"}", - "bin": "/nix/store/pbzb48v0ycf80jgligcp4n8z0rblna4n-advanced-attributes-structured-attrs-bin", - "dev": "/nix/store/7xapi8jv7flcz1qq8jhw55ar8ag8hldh-advanced-attributes-structured-attrs-dev", - "out": "/nix/store/mpq3l1l1qc2yr50q520g08kprprwv79f-advanced-attributes-structured-attrs" - }, - "inputDrvs": { - "/nix/store/4xm4wccqsvagz9gjksn24s7rip2fdy7v-foo.drv": { - "dynamicOutputs": {}, - "outputs": [ - "out" - ] - }, - "/nix/store/plsq5jbr5nhgqwcgb2qxw7jchc09dnl8-bar.drv": { - "dynamicOutputs": {}, - "outputs": [ - "out" - ] - } - }, - "inputSrcs": [], - "name": "advanced-attributes-structured-attrs", - "outputs": { - "bin": { - "path": "/nix/store/pbzb48v0ycf80jgligcp4n8z0rblna4n-advanced-attributes-structured-attrs-bin" - }, - "dev": { - "path": "/nix/store/7xapi8jv7flcz1qq8jhw55ar8ag8hldh-advanced-attributes-structured-attrs-dev" - }, - "out": { - "path": "/nix/store/mpq3l1l1qc2yr50q520g08kprprwv79f-advanced-attributes-structured-attrs" - } - }, - "system": "my-system" -} diff --git a/src/libstore-tests/data/derivation/advanced-attributes.drv b/src/libstore-tests/data/derivation/advanced-attributes.drv deleted file mode 120000 index 1dc394a0a4f..00000000000 --- a/src/libstore-tests/data/derivation/advanced-attributes.drv +++ /dev/null @@ -1 +0,0 @@ -../../../../tests/functional/derivation/advanced-attributes.drv \ No newline at end of file diff --git a/src/libstore-tests/data/derivation/ca/advanced-attributes-defaults.drv b/src/libstore-tests/data/derivation/ca/advanced-attributes-defaults.drv new file mode 120000 index 00000000000..a9b4f7fa745 --- /dev/null +++ b/src/libstore-tests/data/derivation/ca/advanced-attributes-defaults.drv @@ -0,0 +1 @@ +../../../../../tests/functional/derivation/ca/advanced-attributes-defaults.drv \ No newline at end of file diff --git a/src/libstore-tests/data/derivation/ca/advanced-attributes-defaults.json b/src/libstore-tests/data/derivation/ca/advanced-attributes-defaults.json new file mode 100644 index 00000000000..bc67236b54f --- /dev/null +++ b/src/libstore-tests/data/derivation/ca/advanced-attributes-defaults.json @@ -0,0 +1,25 @@ +{ + "args": [ + "-c", + "echo hello > $out" + ], + "builder": "/bin/bash", + "env": { + "builder": "/bin/bash", + "name": "advanced-attributes-defaults", + "out": "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9", + "outputHashAlgo": "sha256", + "outputHashMode": "recursive", + "system": "my-system" + }, + "inputDrvs": {}, + "inputSrcs": [], + "name": "advanced-attributes-defaults", + "outputs": { + "out": { + "hashAlgo": "sha256", + "method": "nar" + } + }, + "system": "my-system" +} diff --git a/src/libstore-tests/data/derivation/ca/advanced-attributes-structured-attrs-defaults.drv b/src/libstore-tests/data/derivation/ca/advanced-attributes-structured-attrs-defaults.drv new file mode 120000 index 00000000000..61da0470a77 --- /dev/null +++ b/src/libstore-tests/data/derivation/ca/advanced-attributes-structured-attrs-defaults.drv @@ -0,0 +1 @@ +../../../../../tests/functional/derivation/ca/advanced-attributes-structured-attrs-defaults.drv \ No newline at end of file diff --git a/src/libstore-tests/data/derivation/ca/advanced-attributes-structured-attrs-defaults.json b/src/libstore-tests/data/derivation/ca/advanced-attributes-structured-attrs-defaults.json new file mode 100644 index 00000000000..183148b29b3 --- /dev/null +++ b/src/libstore-tests/data/derivation/ca/advanced-attributes-structured-attrs-defaults.json @@ -0,0 +1,36 @@ +{ + "args": [ + "-c", + "echo hello > $out" + ], + "builder": "/bin/bash", + "env": { + "dev": "/02qcpld1y6xhs5gz9bchpxaw0xdhmsp5dv88lh25r2ss44kh8dxz", + "out": "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9" + }, + "inputDrvs": {}, + "inputSrcs": [], + "name": "advanced-attributes-structured-attrs-defaults", + "outputs": { + "dev": { + "hashAlgo": "sha256", + "method": "nar" + }, + "out": { + "hashAlgo": "sha256", + "method": "nar" + } + }, + "structuredAttrs": { + "builder": "/bin/bash", + "name": "advanced-attributes-structured-attrs-defaults", + "outputHashAlgo": "sha256", + "outputHashMode": "recursive", + "outputs": [ + "out", + "dev" + ], + "system": "my-system" + }, + "system": "my-system" +} diff --git a/src/libstore-tests/data/derivation/ca/advanced-attributes-structured-attrs.drv b/src/libstore-tests/data/derivation/ca/advanced-attributes-structured-attrs.drv new file mode 120000 index 00000000000..c396ee85363 --- /dev/null +++ b/src/libstore-tests/data/derivation/ca/advanced-attributes-structured-attrs.drv @@ -0,0 +1 @@ +../../../../../tests/functional/derivation/ca/advanced-attributes-structured-attrs.drv \ No newline at end of file diff --git a/src/libstore-tests/data/derivation/ca/advanced-attributes-structured-attrs.json b/src/libstore-tests/data/derivation/ca/advanced-attributes-structured-attrs.json new file mode 100644 index 00000000000..ec044d77877 --- /dev/null +++ b/src/libstore-tests/data/derivation/ca/advanced-attributes-structured-attrs.json @@ -0,0 +1,104 @@ +{ + "args": [ + "-c", + "echo hello > $out" + ], + "builder": "/bin/bash", + "env": { + "bin": "/04f3da1kmbr67m3gzxikmsl4vjz5zf777sv6m14ahv22r65aac9m", + "dev": "/02qcpld1y6xhs5gz9bchpxaw0xdhmsp5dv88lh25r2ss44kh8dxz", + "out": "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9" + }, + "inputDrvs": { + "/nix/store/j56sf12rxpcv5swr14vsjn5cwm6bj03h-foo.drv": { + "dynamicOutputs": {}, + "outputs": [ + "dev", + "out" + ] + }, + "/nix/store/qnml92yh97a6fbrs2m5qg5cqlc8vni58-bar.drv": { + "dynamicOutputs": {}, + "outputs": [ + "dev", + "out" + ] + } + }, + "inputSrcs": [ + "/nix/store/qnml92yh97a6fbrs2m5qg5cqlc8vni58-bar.drv" + ], + "name": "advanced-attributes-structured-attrs", + "outputs": { + "bin": { + "hashAlgo": "sha256", + "method": "nar" + }, + "dev": { + "hashAlgo": "sha256", + "method": "nar" + }, + "out": { + "hashAlgo": "sha256", + "method": "nar" + } + }, + "structuredAttrs": { + "__darwinAllowLocalNetworking": true, + "__impureHostDeps": [ + "/usr/bin/ditto" + ], + "__noChroot": true, + "__sandboxProfile": "sandcastle", + "allowSubstitutes": false, + "builder": "/bin/bash", + "exportReferencesGraph": { + "refs1": [ + "/164j69y6zir9z0339n8pjigg3rckinlr77bxsavzizdaaljb7nh9" + ], + "refs2": [ + "/nix/store/qnml92yh97a6fbrs2m5qg5cqlc8vni58-bar.drv" + ] + }, + "impureEnvVars": [ + "UNICORN" + ], + "name": "advanced-attributes-structured-attrs", + "outputChecks": { + "bin": { + "disallowedReferences": [ + "/0nyw57wm2iicnm9rglvjmbci3ikmcp823czdqdzdcgsnnwqps71g" + ], + "disallowedRequisites": [ + "/07f301yqyz8c6wf6bbbavb2q39j4n8kmcly1s09xadyhgy6x2wr8" + ] + }, + "dev": { + "maxClosureSize": 5909, + "maxSize": 789 + }, + "out": { + "allowedReferences": [ + "/164j69y6zir9z0339n8pjigg3rckinlr77bxsavzizdaaljb7nh9" + ], + "allowedRequisites": [ + "/0nr45p69vn6izw9446wsh9bng9nndhvn19kpsm4n96a5mycw0s4z" + ] + } + }, + "outputHashAlgo": "sha256", + "outputHashMode": "recursive", + "outputs": [ + "out", + "bin", + "dev" + ], + "preferLocalBuild": true, + "requiredSystemFeatures": [ + "rainbow", + "uid-range" + ], + "system": "my-system" + }, + "system": "my-system" +} diff --git a/src/libstore-tests/data/derivation/ca/advanced-attributes.drv b/src/libstore-tests/data/derivation/ca/advanced-attributes.drv new file mode 120000 index 00000000000..acba9064d10 --- /dev/null +++ b/src/libstore-tests/data/derivation/ca/advanced-attributes.drv @@ -0,0 +1 @@ +../../../../../tests/functional/derivation/ca/advanced-attributes.drv \ No newline at end of file diff --git a/src/libstore-tests/data/derivation/ca/advanced-attributes.json b/src/libstore-tests/data/derivation/ca/advanced-attributes.json new file mode 100644 index 00000000000..0ac0a9c5c1c --- /dev/null +++ b/src/libstore-tests/data/derivation/ca/advanced-attributes.json @@ -0,0 +1,55 @@ +{ + "args": [ + "-c", + "echo hello > $out" + ], + "builder": "/bin/bash", + "env": { + "__darwinAllowLocalNetworking": "1", + "__impureHostDeps": "/usr/bin/ditto", + "__noChroot": "1", + "__sandboxProfile": "sandcastle", + "allowSubstitutes": "", + "allowedReferences": "/164j69y6zir9z0339n8pjigg3rckinlr77bxsavzizdaaljb7nh9", + "allowedRequisites": "/0nr45p69vn6izw9446wsh9bng9nndhvn19kpsm4n96a5mycw0s4z", + "builder": "/bin/bash", + "disallowedReferences": "/0nyw57wm2iicnm9rglvjmbci3ikmcp823czdqdzdcgsnnwqps71g", + "disallowedRequisites": "/07f301yqyz8c6wf6bbbavb2q39j4n8kmcly1s09xadyhgy6x2wr8", + "exportReferencesGraph": "refs1 /164j69y6zir9z0339n8pjigg3rckinlr77bxsavzizdaaljb7nh9 refs2 /nix/store/qnml92yh97a6fbrs2m5qg5cqlc8vni58-bar.drv", + "impureEnvVars": "UNICORN", + "name": "advanced-attributes", + "out": "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9", + "outputHashAlgo": "sha256", + "outputHashMode": "recursive", + "preferLocalBuild": "1", + "requiredSystemFeatures": "rainbow uid-range", + "system": "my-system" + }, + "inputDrvs": { + "/nix/store/j56sf12rxpcv5swr14vsjn5cwm6bj03h-foo.drv": { + "dynamicOutputs": {}, + "outputs": [ + "dev", + "out" + ] + }, + "/nix/store/qnml92yh97a6fbrs2m5qg5cqlc8vni58-bar.drv": { + "dynamicOutputs": {}, + "outputs": [ + "dev", + "out" + ] + } + }, + "inputSrcs": [ + "/nix/store/qnml92yh97a6fbrs2m5qg5cqlc8vni58-bar.drv" + ], + "name": "advanced-attributes", + "outputs": { + "out": { + "hashAlgo": "sha256", + "method": "nar" + } + }, + "system": "my-system" +} diff --git a/src/libstore-tests/data/derivation/ia/advanced-attributes-defaults.drv b/src/libstore-tests/data/derivation/ia/advanced-attributes-defaults.drv new file mode 120000 index 00000000000..7f1aa367ed2 --- /dev/null +++ b/src/libstore-tests/data/derivation/ia/advanced-attributes-defaults.drv @@ -0,0 +1 @@ +../../../../../tests/functional/derivation/ia/advanced-attributes-defaults.drv \ No newline at end of file diff --git a/src/libstore-tests/data/derivation/advanced-attributes-defaults.json b/src/libstore-tests/data/derivation/ia/advanced-attributes-defaults.json similarity index 100% rename from src/libstore-tests/data/derivation/advanced-attributes-defaults.json rename to src/libstore-tests/data/derivation/ia/advanced-attributes-defaults.json diff --git a/src/libstore-tests/data/derivation/ia/advanced-attributes-structured-attrs-defaults.drv b/src/libstore-tests/data/derivation/ia/advanced-attributes-structured-attrs-defaults.drv new file mode 120000 index 00000000000..77aa67353a3 --- /dev/null +++ b/src/libstore-tests/data/derivation/ia/advanced-attributes-structured-attrs-defaults.drv @@ -0,0 +1 @@ +../../../../../tests/functional/derivation/ia/advanced-attributes-structured-attrs-defaults.drv \ No newline at end of file diff --git a/src/libstore-tests/data/derivation/advanced-attributes-structured-attrs-defaults.json b/src/libstore-tests/data/derivation/ia/advanced-attributes-structured-attrs-defaults.json similarity index 78% rename from src/libstore-tests/data/derivation/advanced-attributes-structured-attrs-defaults.json rename to src/libstore-tests/data/derivation/ia/advanced-attributes-structured-attrs-defaults.json index 473d006acb5..f5349e6c311 100644 --- a/src/libstore-tests/data/derivation/advanced-attributes-structured-attrs-defaults.json +++ b/src/libstore-tests/data/derivation/ia/advanced-attributes-structured-attrs-defaults.json @@ -5,7 +5,6 @@ ], "builder": "/bin/bash", "env": { - "__json": "{\"builder\":\"/bin/bash\",\"name\":\"advanced-attributes-structured-attrs-defaults\",\"outputs\":[\"out\",\"dev\"],\"system\":\"my-system\"}", "dev": "/nix/store/8bazivnbipbyi569623skw5zm91z6kc2-advanced-attributes-structured-attrs-defaults-dev", "out": "/nix/store/f8f8nvnx32bxvyxyx2ff7akbvwhwd9dw-advanced-attributes-structured-attrs-defaults" }, @@ -20,5 +19,14 @@ "path": "/nix/store/f8f8nvnx32bxvyxyx2ff7akbvwhwd9dw-advanced-attributes-structured-attrs-defaults" } }, + "structuredAttrs": { + "builder": "/bin/bash", + "name": "advanced-attributes-structured-attrs-defaults", + "outputs": [ + "out", + "dev" + ], + "system": "my-system" + }, "system": "my-system" } diff --git a/src/libstore-tests/data/derivation/ia/advanced-attributes-structured-attrs.drv b/src/libstore-tests/data/derivation/ia/advanced-attributes-structured-attrs.drv new file mode 120000 index 00000000000..a4e25feba34 --- /dev/null +++ b/src/libstore-tests/data/derivation/ia/advanced-attributes-structured-attrs.drv @@ -0,0 +1 @@ +../../../../../tests/functional/derivation/ia/advanced-attributes-structured-attrs.drv \ No newline at end of file diff --git a/src/libstore-tests/data/derivation/ia/advanced-attributes-structured-attrs.json b/src/libstore-tests/data/derivation/ia/advanced-attributes-structured-attrs.json new file mode 100644 index 00000000000..b8d56646275 --- /dev/null +++ b/src/libstore-tests/data/derivation/ia/advanced-attributes-structured-attrs.json @@ -0,0 +1,99 @@ +{ + "args": [ + "-c", + "echo hello > $out" + ], + "builder": "/bin/bash", + "env": { + "bin": "/nix/store/33qms3h55wlaspzba3brlzlrm8m2239g-advanced-attributes-structured-attrs-bin", + "dev": "/nix/store/wyfgwsdi8rs851wmy1xfzdxy7y5vrg5l-advanced-attributes-structured-attrs-dev", + "out": "/nix/store/7cxy4zx1vqc885r4jl2l64pymqbdmhii-advanced-attributes-structured-attrs" + }, + "inputDrvs": { + "/nix/store/afc3vbjbzql750v2lp8gxgaxsajphzih-foo.drv": { + "dynamicOutputs": {}, + "outputs": [ + "dev", + "out" + ] + }, + "/nix/store/vj2i49jm2868j2fmqvxm70vlzmzvgv14-bar.drv": { + "dynamicOutputs": {}, + "outputs": [ + "dev", + "out" + ] + } + }, + "inputSrcs": [ + "/nix/store/vj2i49jm2868j2fmqvxm70vlzmzvgv14-bar.drv" + ], + "name": "advanced-attributes-structured-attrs", + "outputs": { + "bin": { + "path": "/nix/store/33qms3h55wlaspzba3brlzlrm8m2239g-advanced-attributes-structured-attrs-bin" + }, + "dev": { + "path": "/nix/store/wyfgwsdi8rs851wmy1xfzdxy7y5vrg5l-advanced-attributes-structured-attrs-dev" + }, + "out": { + "path": "/nix/store/7cxy4zx1vqc885r4jl2l64pymqbdmhii-advanced-attributes-structured-attrs" + } + }, + "structuredAttrs": { + "__darwinAllowLocalNetworking": true, + "__impureHostDeps": [ + "/usr/bin/ditto" + ], + "__noChroot": true, + "__sandboxProfile": "sandcastle", + "allowSubstitutes": false, + "builder": "/bin/bash", + "exportReferencesGraph": { + "refs1": [ + "/nix/store/p0hax2lzvjpfc2gwkk62xdglz0fcqfzn-foo" + ], + "refs2": [ + "/nix/store/vj2i49jm2868j2fmqvxm70vlzmzvgv14-bar.drv" + ] + }, + "impureEnvVars": [ + "UNICORN" + ], + "name": "advanced-attributes-structured-attrs", + "outputChecks": { + "bin": { + "disallowedReferences": [ + "/nix/store/r5cff30838majxk5mp3ip2diffi8vpaj-bar" + ], + "disallowedRequisites": [ + "/nix/store/9b61w26b4avv870dw0ymb6rw4r1hzpws-bar-dev" + ] + }, + "dev": { + "maxClosureSize": 5909, + "maxSize": 789 + }, + "out": { + "allowedReferences": [ + "/nix/store/p0hax2lzvjpfc2gwkk62xdglz0fcqfzn-foo" + ], + "allowedRequisites": [ + "/nix/store/z0rjzy29v9k5qa4nqpykrbzirj7sd43v-foo-dev" + ] + } + }, + "outputs": [ + "out", + "bin", + "dev" + ], + "preferLocalBuild": true, + "requiredSystemFeatures": [ + "rainbow", + "uid-range" + ], + "system": "my-system" + }, + "system": "my-system" +} diff --git a/src/libstore-tests/data/derivation/ia/advanced-attributes.drv b/src/libstore-tests/data/derivation/ia/advanced-attributes.drv new file mode 120000 index 00000000000..ecc2f5f3822 --- /dev/null +++ b/src/libstore-tests/data/derivation/ia/advanced-attributes.drv @@ -0,0 +1 @@ +../../../../../tests/functional/derivation/ia/advanced-attributes.drv \ No newline at end of file diff --git a/src/libstore-tests/data/derivation/ia/advanced-attributes.json b/src/libstore-tests/data/derivation/ia/advanced-attributes.json new file mode 100644 index 00000000000..20ce5e1c2bb --- /dev/null +++ b/src/libstore-tests/data/derivation/ia/advanced-attributes.json @@ -0,0 +1,52 @@ +{ + "args": [ + "-c", + "echo hello > $out" + ], + "builder": "/bin/bash", + "env": { + "__darwinAllowLocalNetworking": "1", + "__impureHostDeps": "/usr/bin/ditto", + "__noChroot": "1", + "__sandboxProfile": "sandcastle", + "allowSubstitutes": "", + "allowedReferences": "/nix/store/p0hax2lzvjpfc2gwkk62xdglz0fcqfzn-foo", + "allowedRequisites": "/nix/store/z0rjzy29v9k5qa4nqpykrbzirj7sd43v-foo-dev", + "builder": "/bin/bash", + "disallowedReferences": "/nix/store/r5cff30838majxk5mp3ip2diffi8vpaj-bar", + "disallowedRequisites": "/nix/store/9b61w26b4avv870dw0ymb6rw4r1hzpws-bar-dev", + "exportReferencesGraph": "refs1 /nix/store/p0hax2lzvjpfc2gwkk62xdglz0fcqfzn-foo refs2 /nix/store/vj2i49jm2868j2fmqvxm70vlzmzvgv14-bar.drv", + "impureEnvVars": "UNICORN", + "name": "advanced-attributes", + "out": "/nix/store/wyhpwd748pns4k7svh48wdrc8kvjk0ra-advanced-attributes", + "preferLocalBuild": "1", + "requiredSystemFeatures": "rainbow uid-range", + "system": "my-system" + }, + "inputDrvs": { + "/nix/store/afc3vbjbzql750v2lp8gxgaxsajphzih-foo.drv": { + "dynamicOutputs": {}, + "outputs": [ + "dev", + "out" + ] + }, + "/nix/store/vj2i49jm2868j2fmqvxm70vlzmzvgv14-bar.drv": { + "dynamicOutputs": {}, + "outputs": [ + "dev", + "out" + ] + } + }, + "inputSrcs": [ + "/nix/store/vj2i49jm2868j2fmqvxm70vlzmzvgv14-bar.drv" + ], + "name": "advanced-attributes", + "outputs": { + "out": { + "path": "/nix/store/wyhpwd748pns4k7svh48wdrc8kvjk0ra-advanced-attributes" + } + }, + "system": "my-system" +} diff --git a/src/libstore-tests/derivation-advanced-attrs.cc b/src/libstore-tests/derivation-advanced-attrs.cc index 9d2c64ef3e4..b68134cd1cc 100644 --- a/src/libstore-tests/derivation-advanced-attrs.cc +++ b/src/libstore-tests/derivation-advanced-attrs.cc @@ -1,14 +1,16 @@ #include #include -#include "experimental-features.hh" -#include "derivations.hh" +#include "nix/util/experimental-features.hh" +#include "nix/store/derivations.hh" +#include "nix/store/derivations.hh" +#include "nix/store/derivation-options.hh" +#include "nix/store/parsed-derivations.hh" +#include "nix/util/types.hh" +#include "nix/util/json-utils.hh" -#include "tests/libstore.hh" -#include "tests/characterization.hh" -#include "parsed-derivations.hh" -#include "types.hh" -#include "json-utils.hh" +#include "nix/store/tests/libstore.hh" +#include "nix/util/tests/characterization.hh" namespace nix { @@ -16,218 +18,482 @@ using nlohmann::json; class DerivationAdvancedAttrsTest : public CharacterizationTest, public LibStoreTest { - std::filesystem::path unitTestData = getUnitTestData() / "derivation"; +protected: + std::filesystem::path unitTestData = getUnitTestData() / "derivation" / "ia"; public: std::filesystem::path goldenMaster(std::string_view testStem) const override { return unitTestData / testStem; } + + /** + * We set these in tests rather than the regular globals so we don't have + * to worry about race conditions if the tests run concurrently. + */ + ExperimentalFeatureSettings mockXpSettings; +}; + +class CaDerivationAdvancedAttrsTest : public DerivationAdvancedAttrsTest +{ + void SetUp() override + { + unitTestData = getUnitTestData() / "derivation" / "ca"; + mockXpSettings.set("experimental-features", "ca-derivations"); + } }; -#define TEST_ATERM_JSON(STEM, NAME) \ - TEST_F(DerivationAdvancedAttrsTest, Derivation_##STEM##_from_json) \ - { \ - readTest(NAME ".json", [&](const auto & encoded_) { \ - auto encoded = json::parse(encoded_); \ - /* Use DRV file instead of C++ literal as source of truth. */ \ - auto aterm = readFile(goldenMaster(NAME ".drv")); \ - auto expected = parseDerivation(*store, std::move(aterm), NAME); \ - Derivation got = Derivation::fromJSON(*store, encoded); \ - EXPECT_EQ(got, expected); \ - }); \ - } \ - \ - TEST_F(DerivationAdvancedAttrsTest, Derivation_##STEM##_to_json) \ - { \ - writeTest( \ - NAME ".json", \ - [&]() -> json { \ - /* Use DRV file instead of C++ literal as source of truth. */ \ - auto aterm = readFile(goldenMaster(NAME ".drv")); \ - return parseDerivation(*store, std::move(aterm), NAME).toJSON(*store); \ - }, \ - [](const auto & file) { return json::parse(readFile(file)); }, \ - [](const auto & file, const auto & got) { return writeFile(file, got.dump(2) + "\n"); }); \ - } \ - \ - TEST_F(DerivationAdvancedAttrsTest, Derivation_##STEM##_from_aterm) \ - { \ - readTest(NAME ".drv", [&](auto encoded) { \ - /* Use JSON file instead of C++ literal as source of truth. */ \ - auto json = json::parse(readFile(goldenMaster(NAME ".json"))); \ - auto expected = Derivation::fromJSON(*store, json); \ - auto got = parseDerivation(*store, std::move(encoded), NAME); \ - EXPECT_EQ(got.toJSON(*store), expected.toJSON(*store)); \ - EXPECT_EQ(got, expected); \ - }); \ - } \ - \ +template +class DerivationAdvancedAttrsBothTest : public Fixture +{}; + +using BothFixtures = ::testing::Types; + +TYPED_TEST_SUITE(DerivationAdvancedAttrsBothTest, BothFixtures); + +#define TEST_ATERM_JSON(STEM, NAME) \ + TYPED_TEST(DerivationAdvancedAttrsBothTest, Derivation_##STEM##_from_json) \ + { \ + this->readTest(NAME ".json", [&](const auto & encoded_) { \ + auto encoded = json::parse(encoded_); \ + /* Use DRV file instead of C++ literal as source of truth. */ \ + auto aterm = readFile(this->goldenMaster(NAME ".drv")); \ + auto expected = parseDerivation(*this->store, std::move(aterm), NAME, this->mockXpSettings); \ + Derivation got = Derivation::fromJSON(*this->store, encoded, this->mockXpSettings); \ + EXPECT_EQ(got, expected); \ + }); \ + } \ + \ + TYPED_TEST(DerivationAdvancedAttrsBothTest, Derivation_##STEM##_to_json) \ + { \ + this->writeTest( \ + NAME ".json", \ + [&]() -> json { \ + /* Use DRV file instead of C++ literal as source of truth. */ \ + auto aterm = readFile(this->goldenMaster(NAME ".drv")); \ + return parseDerivation(*this->store, std::move(aterm), NAME, this->mockXpSettings) \ + .toJSON(*this->store); \ + }, \ + [](const auto & file) { return json::parse(readFile(file)); }, \ + [](const auto & file, const auto & got) { return writeFile(file, got.dump(2) + "\n"); }); \ + } \ + \ + TYPED_TEST(DerivationAdvancedAttrsBothTest, Derivation_##STEM##_from_aterm) \ + { \ + this->readTest(NAME ".drv", [&](auto encoded) { \ + /* Use JSON file instead of C++ literal as source of truth. */ \ + auto json = json::parse(readFile(this->goldenMaster(NAME ".json"))); \ + auto expected = Derivation::fromJSON(*this->store, json, this->mockXpSettings); \ + auto got = parseDerivation(*this->store, std::move(encoded), NAME, this->mockXpSettings); \ + EXPECT_EQ(got.toJSON(*this->store), expected.toJSON(*this->store)); \ + EXPECT_EQ(got, expected); \ + }); \ + } \ + \ /* No corresponding write test, because we need to read the drv to write the json file */ -TEST_ATERM_JSON(advancedAttributes_defaults, "advanced-attributes-defaults"); TEST_ATERM_JSON(advancedAttributes, "advanced-attributes-defaults"); -TEST_ATERM_JSON(advancedAttributes_structuredAttrs_defaults, "advanced-attributes-structured-attrs"); +TEST_ATERM_JSON(advancedAttributes_defaults, "advanced-attributes"); TEST_ATERM_JSON(advancedAttributes_structuredAttrs, "advanced-attributes-structured-attrs-defaults"); +TEST_ATERM_JSON(advancedAttributes_structuredAttrs_defaults, "advanced-attributes-structured-attrs"); #undef TEST_ATERM_JSON -TEST_F(DerivationAdvancedAttrsTest, Derivation_advancedAttributes_defaults) +using ExportReferencesMap = decltype(DerivationOptions::exportReferencesGraph); + +TYPED_TEST(DerivationAdvancedAttrsBothTest, advancedAttributes_defaults) { - readTest("advanced-attributes-defaults.drv", [&](auto encoded) { - auto got = parseDerivation(*store, std::move(encoded), "foo"); - - auto drvPath = writeDerivation(*store, got, NoRepair, true); - - ParsedDerivation parsedDrv(drvPath, got); - - EXPECT_EQ(parsedDrv.getStringAttr("__sandboxProfile").value_or(""), ""); - EXPECT_EQ(parsedDrv.getBoolAttr("__noChroot"), false); - EXPECT_EQ(parsedDrv.getStringsAttr("__impureHostDeps").value_or(Strings()), Strings()); - EXPECT_EQ(parsedDrv.getStringsAttr("impureEnvVars").value_or(Strings()), Strings()); - EXPECT_EQ(parsedDrv.getBoolAttr("__darwinAllowLocalNetworking"), false); - EXPECT_EQ(parsedDrv.getStringsAttr("allowedReferences"), std::nullopt); - EXPECT_EQ(parsedDrv.getStringsAttr("allowedRequisites"), std::nullopt); - EXPECT_EQ(parsedDrv.getStringsAttr("disallowedReferences"), std::nullopt); - EXPECT_EQ(parsedDrv.getStringsAttr("disallowedRequisites"), std::nullopt); - EXPECT_EQ(parsedDrv.getRequiredSystemFeatures(), StringSet()); - EXPECT_EQ(parsedDrv.canBuildLocally(*store), false); - EXPECT_EQ(parsedDrv.willBuildLocally(*store), false); - EXPECT_EQ(parsedDrv.substitutesAllowed(), true); - EXPECT_EQ(parsedDrv.useUidRange(), false); + this->readTest("advanced-attributes-defaults.drv", [&](auto encoded) { + auto got = parseDerivation(*this->store, std::move(encoded), "foo", this->mockXpSettings); + + auto drvPath = writeDerivation(*this->store, got, NoRepair, true); + + auto parsedDrv = StructuredAttrs::tryParse(got.env); + DerivationOptions options = DerivationOptions::fromStructuredAttrs(got.env, parsedDrv ? &*parsedDrv : nullptr); + + EXPECT_TRUE(!parsedDrv); + + EXPECT_EQ(options.additionalSandboxProfile, ""); + EXPECT_EQ(options.noChroot, false); + EXPECT_EQ(options.impureHostDeps, StringSet{}); + EXPECT_EQ(options.impureEnvVars, StringSet{}); + EXPECT_EQ(options.allowLocalNetworking, false); + EXPECT_EQ(options.exportReferencesGraph, ExportReferencesMap{}); + { + auto * checksForAllOutputs_ = std::get_if<0>(&options.outputChecks); + ASSERT_TRUE(checksForAllOutputs_ != nullptr); + auto & checksForAllOutputs = *checksForAllOutputs_; + + EXPECT_EQ(checksForAllOutputs.allowedReferences, std::nullopt); + EXPECT_EQ(checksForAllOutputs.allowedRequisites, std::nullopt); + EXPECT_EQ(checksForAllOutputs.disallowedReferences, StringSet{}); + EXPECT_EQ(checksForAllOutputs.disallowedRequisites, StringSet{}); + } + EXPECT_EQ(options.canBuildLocally(*this->store, got), false); + EXPECT_EQ(options.willBuildLocally(*this->store, got), false); + EXPECT_EQ(options.substitutesAllowed(), true); + EXPECT_EQ(options.useUidRange(got), false); }); }; -TEST_F(DerivationAdvancedAttrsTest, Derivation_advancedAttributes) +TEST_F(DerivationAdvancedAttrsTest, advancedAttributes_defaults) { - readTest("advanced-attributes.drv", [&](auto encoded) { - auto got = parseDerivation(*store, std::move(encoded), "foo"); + this->readTest("advanced-attributes-defaults.drv", [&](auto encoded) { + auto got = parseDerivation(*this->store, std::move(encoded), "foo", this->mockXpSettings); - auto drvPath = writeDerivation(*store, got, NoRepair, true); + auto drvPath = writeDerivation(*this->store, got, NoRepair, true); - ParsedDerivation parsedDrv(drvPath, got); + auto parsedDrv = StructuredAttrs::tryParse(got.env); + DerivationOptions options = DerivationOptions::fromStructuredAttrs(got.env, parsedDrv ? &*parsedDrv : nullptr); - StringSet systemFeatures{"rainbow", "uid-range"}; + EXPECT_EQ(options.getRequiredSystemFeatures(got), StringSet{}); + }); +}; + +TEST_F(CaDerivationAdvancedAttrsTest, advancedAttributes_defaults) +{ + this->readTest("advanced-attributes-defaults.drv", [&](auto encoded) { + auto got = parseDerivation(*this->store, std::move(encoded), "foo", this->mockXpSettings); + + auto drvPath = writeDerivation(*this->store, got, NoRepair, true); + + auto parsedDrv = StructuredAttrs::tryParse(got.env); + DerivationOptions options = DerivationOptions::fromStructuredAttrs(got.env, parsedDrv ? &*parsedDrv : nullptr); + + EXPECT_EQ(options.getRequiredSystemFeatures(got), StringSet{"ca-derivations"}); + }); +}; + +TYPED_TEST(DerivationAdvancedAttrsBothTest, advancedAttributes) +{ + this->readTest("advanced-attributes.drv", [&](auto encoded) { + auto got = parseDerivation(*this->store, std::move(encoded), "foo", this->mockXpSettings); + + auto drvPath = writeDerivation(*this->store, got, NoRepair, true); + + auto parsedDrv = StructuredAttrs::tryParse(got.env); + DerivationOptions options = DerivationOptions::fromStructuredAttrs(got.env, parsedDrv ? &*parsedDrv : nullptr); + + EXPECT_TRUE(!parsedDrv); + + EXPECT_EQ(options.additionalSandboxProfile, "sandcastle"); + EXPECT_EQ(options.noChroot, true); + EXPECT_EQ(options.impureHostDeps, StringSet{"/usr/bin/ditto"}); + EXPECT_EQ(options.impureEnvVars, StringSet{"UNICORN"}); + EXPECT_EQ(options.allowLocalNetworking, true); + EXPECT_EQ(options.canBuildLocally(*this->store, got), false); + EXPECT_EQ(options.willBuildLocally(*this->store, got), false); + EXPECT_EQ(options.substitutesAllowed(), false); + EXPECT_EQ(options.useUidRange(got), true); + }); +}; + +TEST_F(DerivationAdvancedAttrsTest, advancedAttributes) +{ + this->readTest("advanced-attributes.drv", [&](auto encoded) { + auto got = parseDerivation(*this->store, std::move(encoded), "foo", this->mockXpSettings); + + auto drvPath = writeDerivation(*this->store, got, NoRepair, true); + + auto parsedDrv = StructuredAttrs::tryParse(got.env); + DerivationOptions options = DerivationOptions::fromStructuredAttrs(got.env, parsedDrv ? &*parsedDrv : nullptr); - EXPECT_EQ(parsedDrv.getStringAttr("__sandboxProfile").value_or(""), "sandcastle"); - EXPECT_EQ(parsedDrv.getBoolAttr("__noChroot"), true); - EXPECT_EQ(parsedDrv.getStringsAttr("__impureHostDeps").value_or(Strings()), Strings{"/usr/bin/ditto"}); - EXPECT_EQ(parsedDrv.getStringsAttr("impureEnvVars").value_or(Strings()), Strings{"UNICORN"}); - EXPECT_EQ(parsedDrv.getBoolAttr("__darwinAllowLocalNetworking"), true); - EXPECT_EQ( - parsedDrv.getStringsAttr("allowedReferences"), Strings{"/nix/store/3c08bzb71z4wiag719ipjxr277653ynp-foo"}); - EXPECT_EQ( - parsedDrv.getStringsAttr("allowedRequisites"), Strings{"/nix/store/3c08bzb71z4wiag719ipjxr277653ynp-foo"}); EXPECT_EQ( - parsedDrv.getStringsAttr("disallowedReferences"), - Strings{"/nix/store/7rhsm8i393hm1wcsmph782awg1hi2f7x-bar"}); + options.exportReferencesGraph, + (ExportReferencesMap{ + { + "refs1", + { + "/nix/store/p0hax2lzvjpfc2gwkk62xdglz0fcqfzn-foo", + }, + }, + { + "refs2", + { + "/nix/store/vj2i49jm2868j2fmqvxm70vlzmzvgv14-bar.drv", + }, + }, + })); + + { + auto * checksForAllOutputs_ = std::get_if<0>(&options.outputChecks); + ASSERT_TRUE(checksForAllOutputs_ != nullptr); + auto & checksForAllOutputs = *checksForAllOutputs_; + + EXPECT_EQ( + checksForAllOutputs.allowedReferences, StringSet{"/nix/store/p0hax2lzvjpfc2gwkk62xdglz0fcqfzn-foo"}); + EXPECT_EQ( + checksForAllOutputs.allowedRequisites, + StringSet{"/nix/store/z0rjzy29v9k5qa4nqpykrbzirj7sd43v-foo-dev"}); + EXPECT_EQ( + checksForAllOutputs.disallowedReferences, StringSet{"/nix/store/r5cff30838majxk5mp3ip2diffi8vpaj-bar"}); + EXPECT_EQ( + checksForAllOutputs.disallowedRequisites, + StringSet{"/nix/store/9b61w26b4avv870dw0ymb6rw4r1hzpws-bar-dev"}); + } + + StringSet systemFeatures{"rainbow", "uid-range"}; + + EXPECT_EQ(options.getRequiredSystemFeatures(got), systemFeatures); + }); +}; + +TEST_F(CaDerivationAdvancedAttrsTest, advancedAttributes) +{ + this->readTest("advanced-attributes.drv", [&](auto encoded) { + auto got = parseDerivation(*this->store, std::move(encoded), "foo", this->mockXpSettings); + + auto drvPath = writeDerivation(*this->store, got, NoRepair, true); + + auto parsedDrv = StructuredAttrs::tryParse(got.env); + DerivationOptions options = DerivationOptions::fromStructuredAttrs(got.env, parsedDrv ? &*parsedDrv : nullptr); + EXPECT_EQ( - parsedDrv.getStringsAttr("disallowedRequisites"), - Strings{"/nix/store/7rhsm8i393hm1wcsmph782awg1hi2f7x-bar"}); - EXPECT_EQ(parsedDrv.getRequiredSystemFeatures(), systemFeatures); - EXPECT_EQ(parsedDrv.canBuildLocally(*store), false); - EXPECT_EQ(parsedDrv.willBuildLocally(*store), false); - EXPECT_EQ(parsedDrv.substitutesAllowed(), false); - EXPECT_EQ(parsedDrv.useUidRange(), true); + options.exportReferencesGraph, + (ExportReferencesMap{ + { + "refs1", + { + "/164j69y6zir9z0339n8pjigg3rckinlr77bxsavzizdaaljb7nh9", + }, + }, + { + "refs2", + { + "/nix/store/qnml92yh97a6fbrs2m5qg5cqlc8vni58-bar.drv", + }, + }, + })); + + { + auto * checksForAllOutputs_ = std::get_if<0>(&options.outputChecks); + ASSERT_TRUE(checksForAllOutputs_ != nullptr); + auto & checksForAllOutputs = *checksForAllOutputs_; + + EXPECT_EQ( + checksForAllOutputs.allowedReferences, + StringSet{"/164j69y6zir9z0339n8pjigg3rckinlr77bxsavzizdaaljb7nh9"}); + EXPECT_EQ( + checksForAllOutputs.allowedRequisites, + StringSet{"/0nr45p69vn6izw9446wsh9bng9nndhvn19kpsm4n96a5mycw0s4z"}); + EXPECT_EQ( + checksForAllOutputs.disallowedReferences, + StringSet{"/0nyw57wm2iicnm9rglvjmbci3ikmcp823czdqdzdcgsnnwqps71g"}); + EXPECT_EQ( + checksForAllOutputs.disallowedRequisites, + StringSet{"/07f301yqyz8c6wf6bbbavb2q39j4n8kmcly1s09xadyhgy6x2wr8"}); + } + + StringSet systemFeatures{"rainbow", "uid-range"}; + systemFeatures.insert("ca-derivations"); + + EXPECT_EQ(options.getRequiredSystemFeatures(got), systemFeatures); }); }; -TEST_F(DerivationAdvancedAttrsTest, Derivation_advancedAttributes_structuredAttrs_defaults) +TYPED_TEST(DerivationAdvancedAttrsBothTest, advancedAttributes_structuredAttrs_defaults) { - readTest("advanced-attributes-structured-attrs-defaults.drv", [&](auto encoded) { - auto got = parseDerivation(*store, std::move(encoded), "foo"); + this->readTest("advanced-attributes-structured-attrs-defaults.drv", [&](auto encoded) { + auto got = parseDerivation(*this->store, std::move(encoded), "foo", this->mockXpSettings); + + auto drvPath = writeDerivation(*this->store, got, NoRepair, true); - auto drvPath = writeDerivation(*store, got, NoRepair, true); + auto parsedDrv = StructuredAttrs::tryParse(got.env); + DerivationOptions options = DerivationOptions::fromStructuredAttrs(got.env, parsedDrv ? &*parsedDrv : nullptr); - ParsedDerivation parsedDrv(drvPath, got); + EXPECT_TRUE(parsedDrv); - EXPECT_EQ(parsedDrv.getStringAttr("__sandboxProfile").value_or(""), ""); - EXPECT_EQ(parsedDrv.getBoolAttr("__noChroot"), false); - EXPECT_EQ(parsedDrv.getStringsAttr("__impureHostDeps").value_or(Strings()), Strings()); - EXPECT_EQ(parsedDrv.getStringsAttr("impureEnvVars").value_or(Strings()), Strings()); - EXPECT_EQ(parsedDrv.getBoolAttr("__darwinAllowLocalNetworking"), false); + EXPECT_EQ(options.additionalSandboxProfile, ""); + EXPECT_EQ(options.noChroot, false); + EXPECT_EQ(options.impureHostDeps, StringSet{}); + EXPECT_EQ(options.impureEnvVars, StringSet{}); + EXPECT_EQ(options.allowLocalNetworking, false); + EXPECT_EQ(options.exportReferencesGraph, ExportReferencesMap{}); { - auto structuredAttrs_ = parsedDrv.getStructuredAttrs(); - ASSERT_TRUE(structuredAttrs_); - auto & structuredAttrs = *structuredAttrs_; + auto * checksPerOutput_ = std::get_if<1>(&options.outputChecks); + ASSERT_TRUE(checksPerOutput_ != nullptr); + auto & checksPerOutput = *checksPerOutput_; - auto outputChecks_ = get(structuredAttrs, "outputChecks"); - ASSERT_FALSE(outputChecks_); + EXPECT_EQ(checksPerOutput.size(), 0u); } - EXPECT_EQ(parsedDrv.getRequiredSystemFeatures(), StringSet()); - EXPECT_EQ(parsedDrv.canBuildLocally(*store), false); - EXPECT_EQ(parsedDrv.willBuildLocally(*store), false); - EXPECT_EQ(parsedDrv.substitutesAllowed(), true); - EXPECT_EQ(parsedDrv.useUidRange(), false); + EXPECT_EQ(options.canBuildLocally(*this->store, got), false); + EXPECT_EQ(options.willBuildLocally(*this->store, got), false); + EXPECT_EQ(options.substitutesAllowed(), true); + EXPECT_EQ(options.useUidRange(got), false); }); }; -TEST_F(DerivationAdvancedAttrsTest, Derivation_advancedAttributes_structuredAttrs) +TEST_F(DerivationAdvancedAttrsTest, advancedAttributes_structuredAttrs_defaults) { - readTest("advanced-attributes-structured-attrs.drv", [&](auto encoded) { - auto got = parseDerivation(*store, std::move(encoded), "foo"); + this->readTest("advanced-attributes-structured-attrs-defaults.drv", [&](auto encoded) { + auto got = parseDerivation(*this->store, std::move(encoded), "foo", this->mockXpSettings); - auto drvPath = writeDerivation(*store, got, NoRepair, true); + auto drvPath = writeDerivation(*this->store, got, NoRepair, true); - ParsedDerivation parsedDrv(drvPath, got); + auto parsedDrv = StructuredAttrs::tryParse(got.env); + DerivationOptions options = DerivationOptions::fromStructuredAttrs(got.env, parsedDrv ? &*parsedDrv : nullptr); - StringSet systemFeatures{"rainbow", "uid-range"}; + EXPECT_EQ(options.getRequiredSystemFeatures(got), StringSet{}); + }); +}; + +TEST_F(CaDerivationAdvancedAttrsTest, advancedAttributes_structuredAttrs_defaults) +{ + this->readTest("advanced-attributes-structured-attrs-defaults.drv", [&](auto encoded) { + auto got = parseDerivation(*this->store, std::move(encoded), "foo", this->mockXpSettings); + + auto drvPath = writeDerivation(*this->store, got, NoRepair, true); + + auto parsedDrv = StructuredAttrs::tryParse(got.env); + DerivationOptions options = DerivationOptions::fromStructuredAttrs(got.env, parsedDrv ? &*parsedDrv : nullptr); + + EXPECT_EQ(options.getRequiredSystemFeatures(got), StringSet{"ca-derivations"}); + }); +}; + +TYPED_TEST(DerivationAdvancedAttrsBothTest, advancedAttributes_structuredAttrs) +{ + this->readTest("advanced-attributes-structured-attrs.drv", [&](auto encoded) { + auto got = parseDerivation(*this->store, std::move(encoded), "foo", this->mockXpSettings); + + auto drvPath = writeDerivation(*this->store, got, NoRepair, true); - EXPECT_EQ(parsedDrv.getStringAttr("__sandboxProfile").value_or(""), "sandcastle"); - EXPECT_EQ(parsedDrv.getBoolAttr("__noChroot"), true); - EXPECT_EQ(parsedDrv.getStringsAttr("__impureHostDeps").value_or(Strings()), Strings{"/usr/bin/ditto"}); - EXPECT_EQ(parsedDrv.getStringsAttr("impureEnvVars").value_or(Strings()), Strings{"UNICORN"}); - EXPECT_EQ(parsedDrv.getBoolAttr("__darwinAllowLocalNetworking"), true); + auto parsedDrv = StructuredAttrs::tryParse(got.env); + DerivationOptions options = DerivationOptions::fromStructuredAttrs(got.env, parsedDrv ? &*parsedDrv : nullptr); + + EXPECT_TRUE(parsedDrv); + + EXPECT_EQ(options.additionalSandboxProfile, "sandcastle"); + EXPECT_EQ(options.noChroot, true); + EXPECT_EQ(options.impureHostDeps, StringSet{"/usr/bin/ditto"}); + EXPECT_EQ(options.impureEnvVars, StringSet{"UNICORN"}); + EXPECT_EQ(options.allowLocalNetworking, true); { - auto structuredAttrs_ = parsedDrv.getStructuredAttrs(); - ASSERT_TRUE(structuredAttrs_); - auto & structuredAttrs = *structuredAttrs_; + auto output_ = get(std::get<1>(options.outputChecks), "dev"); + ASSERT_TRUE(output_); + auto & output = *output_; + + EXPECT_EQ(output.maxSize, 789); + EXPECT_EQ(output.maxClosureSize, 5909); + } + + EXPECT_EQ(options.canBuildLocally(*this->store, got), false); + EXPECT_EQ(options.willBuildLocally(*this->store, got), false); + EXPECT_EQ(options.substitutesAllowed(), false); + EXPECT_EQ(options.useUidRange(got), true); + }); +}; - auto outputChecks_ = get(structuredAttrs, "outputChecks"); - ASSERT_TRUE(outputChecks_); - auto & outputChecks = *outputChecks_; +TEST_F(DerivationAdvancedAttrsTest, advancedAttributes_structuredAttrs) +{ + this->readTest("advanced-attributes-structured-attrs.drv", [&](auto encoded) { + auto got = parseDerivation(*this->store, std::move(encoded), "foo", this->mockXpSettings); + + auto drvPath = writeDerivation(*this->store, got, NoRepair, true); + auto parsedDrv = StructuredAttrs::tryParse(got.env); + DerivationOptions options = DerivationOptions::fromStructuredAttrs(got.env, parsedDrv ? &*parsedDrv : nullptr); + + EXPECT_EQ( + options.exportReferencesGraph, + (ExportReferencesMap{ + { + "refs1", + { + "/nix/store/p0hax2lzvjpfc2gwkk62xdglz0fcqfzn-foo", + }, + }, + { + "refs2", + { + "/nix/store/vj2i49jm2868j2fmqvxm70vlzmzvgv14-bar.drv", + }, + }, + })); + + { { - auto output_ = get(outputChecks, "out"); + auto output_ = get(std::get<1>(options.outputChecks), "out"); ASSERT_TRUE(output_); auto & output = *output_; - EXPECT_EQ( - get(output, "allowedReferences")->get(), - Strings{"/nix/store/3c08bzb71z4wiag719ipjxr277653ynp-foo"}); - EXPECT_EQ( - get(output, "allowedRequisites")->get(), - Strings{"/nix/store/3c08bzb71z4wiag719ipjxr277653ynp-foo"}); + + EXPECT_EQ(output.allowedReferences, StringSet{"/nix/store/p0hax2lzvjpfc2gwkk62xdglz0fcqfzn-foo"}); + EXPECT_EQ(output.allowedRequisites, StringSet{"/nix/store/z0rjzy29v9k5qa4nqpykrbzirj7sd43v-foo-dev"}); } { - auto output_ = get(outputChecks, "bin"); + auto output_ = get(std::get<1>(options.outputChecks), "bin"); ASSERT_TRUE(output_); auto & output = *output_; + + EXPECT_EQ(output.disallowedReferences, StringSet{"/nix/store/r5cff30838majxk5mp3ip2diffi8vpaj-bar"}); EXPECT_EQ( - get(output, "disallowedReferences")->get(), - Strings{"/nix/store/7rhsm8i393hm1wcsmph782awg1hi2f7x-bar"}); - EXPECT_EQ( - get(output, "disallowedRequisites")->get(), - Strings{"/nix/store/7rhsm8i393hm1wcsmph782awg1hi2f7x-bar"}); + output.disallowedRequisites, StringSet{"/nix/store/9b61w26b4avv870dw0ymb6rw4r1hzpws-bar-dev"}); + } + } + + StringSet systemFeatures{"rainbow", "uid-range"}; + + EXPECT_EQ(options.getRequiredSystemFeatures(got), systemFeatures); + }); +}; + +TEST_F(CaDerivationAdvancedAttrsTest, advancedAttributes_structuredAttrs) +{ + this->readTest("advanced-attributes-structured-attrs.drv", [&](auto encoded) { + auto got = parseDerivation(*this->store, std::move(encoded), "foo", this->mockXpSettings); + + auto drvPath = writeDerivation(*this->store, got, NoRepair, true); + + auto parsedDrv = StructuredAttrs::tryParse(got.env); + DerivationOptions options = DerivationOptions::fromStructuredAttrs(got.env, parsedDrv ? &*parsedDrv : nullptr); + + EXPECT_EQ( + options.exportReferencesGraph, + (ExportReferencesMap{ + { + "refs1", + { + "/164j69y6zir9z0339n8pjigg3rckinlr77bxsavzizdaaljb7nh9", + }, + }, + { + "refs2", + { + "/nix/store/qnml92yh97a6fbrs2m5qg5cqlc8vni58-bar.drv", + }, + }, + })); + + { + { + auto output_ = get(std::get<1>(options.outputChecks), "out"); + ASSERT_TRUE(output_); + auto & output = *output_; + + EXPECT_EQ(output.allowedReferences, StringSet{"/164j69y6zir9z0339n8pjigg3rckinlr77bxsavzizdaaljb7nh9"}); + EXPECT_EQ(output.allowedRequisites, StringSet{"/0nr45p69vn6izw9446wsh9bng9nndhvn19kpsm4n96a5mycw0s4z"}); } { - auto output_ = get(outputChecks, "dev"); + auto output_ = get(std::get<1>(options.outputChecks), "bin"); ASSERT_TRUE(output_); auto & output = *output_; - EXPECT_EQ(get(output, "maxSize")->get(), 789); - EXPECT_EQ(get(output, "maxClosureSize")->get(), 5909); + + EXPECT_EQ( + output.disallowedReferences, StringSet{"/0nyw57wm2iicnm9rglvjmbci3ikmcp823czdqdzdcgsnnwqps71g"}); + EXPECT_EQ( + output.disallowedRequisites, StringSet{"/07f301yqyz8c6wf6bbbavb2q39j4n8kmcly1s09xadyhgy6x2wr8"}); } } - EXPECT_EQ(parsedDrv.getRequiredSystemFeatures(), systemFeatures); - EXPECT_EQ(parsedDrv.canBuildLocally(*store), false); - EXPECT_EQ(parsedDrv.willBuildLocally(*store), false); - EXPECT_EQ(parsedDrv.substitutesAllowed(), false); - EXPECT_EQ(parsedDrv.useUidRange(), true); + StringSet systemFeatures{"rainbow", "uid-range"}; + systemFeatures.insert("ca-derivations"); + + EXPECT_EQ(options.getRequiredSystemFeatures(got), systemFeatures); }); }; diff --git a/src/libstore-tests/derivation.cc b/src/libstore-tests/derivation.cc index 14652921abc..fa6711d400d 100644 --- a/src/libstore-tests/derivation.cc +++ b/src/libstore-tests/derivation.cc @@ -1,11 +1,11 @@ #include #include -#include "experimental-features.hh" -#include "derivations.hh" +#include "nix/util/experimental-features.hh" +#include "nix/store/derivations.hh" -#include "tests/libstore.hh" -#include "tests/characterization.hh" +#include "nix/store/tests/libstore.hh" +#include "nix/util/tests/characterization.hh" namespace nix { diff --git a/src/libstore-tests/derived-path.cc b/src/libstore-tests/derived-path.cc index c62d79a78ca..51df2519871 100644 --- a/src/libstore-tests/derived-path.cc +++ b/src/libstore-tests/derived-path.cc @@ -3,8 +3,8 @@ #include #include -#include "tests/derived-path.hh" -#include "tests/libstore.hh" +#include "nix/store/tests/derived-path.hh" +#include "nix/store/tests/libstore.hh" namespace nix { @@ -79,12 +79,19 @@ TEST_F(DerivedPathTest, built_built_xp) { #ifndef COVERAGE +/* TODO: Disabled due to the following error: + + path '00000000000000000000000000000000-0^0' is not a valid store path: + name '0^0' contains illegal character '^' +*/ RC_GTEST_FIXTURE_PROP( DerivedPathTest, - prop_legacy_round_rip, + DISABLED_prop_legacy_round_rip, (const DerivedPath & o)) { - RC_ASSERT(o == DerivedPath::parseLegacy(*store, o.to_string_legacy(*store))); + ExperimentalFeatureSettings xpSettings; + xpSettings.set("experimental-features", "dynamic-derivations"); + RC_ASSERT(o == DerivedPath::parseLegacy(*store, o.to_string_legacy(*store), xpSettings)); } RC_GTEST_FIXTURE_PROP( @@ -92,7 +99,9 @@ RC_GTEST_FIXTURE_PROP( prop_round_rip, (const DerivedPath & o)) { - RC_ASSERT(o == DerivedPath::parse(*store, o.to_string(*store))); + ExperimentalFeatureSettings xpSettings; + xpSettings.set("experimental-features", "dynamic-derivations"); + RC_ASSERT(o == DerivedPath::parse(*store, o.to_string(*store), xpSettings)); } #endif diff --git a/src/libstore-tests/downstream-placeholder.cc b/src/libstore-tests/downstream-placeholder.cc index fd29530acfc..604c8001726 100644 --- a/src/libstore-tests/downstream-placeholder.cc +++ b/src/libstore-tests/downstream-placeholder.cc @@ -1,6 +1,6 @@ #include -#include "downstream-placeholder.hh" +#include "nix/store/downstream-placeholder.hh" namespace nix { diff --git a/src/libstore-tests/http-binary-cache-store.cc b/src/libstore-tests/http-binary-cache-store.cc index 1e415f6251a..f4a3408b587 100644 --- a/src/libstore-tests/http-binary-cache-store.cc +++ b/src/libstore-tests/http-binary-cache-store.cc @@ -1,6 +1,6 @@ #include -#include "http-binary-cache-store.hh" +#include "nix/store/http-binary-cache-store.hh" namespace nix { diff --git a/src/libstore-tests/legacy-ssh-store.cc b/src/libstore-tests/legacy-ssh-store.cc index eb31a240804..158da2831ac 100644 --- a/src/libstore-tests/legacy-ssh-store.cc +++ b/src/libstore-tests/legacy-ssh-store.cc @@ -1,6 +1,6 @@ #include -#include "legacy-ssh-store.hh" +#include "nix/store/legacy-ssh-store.hh" namespace nix { diff --git a/src/libstore-tests/local-binary-cache-store.cc b/src/libstore-tests/local-binary-cache-store.cc index 2e840228dad..01f514e89aa 100644 --- a/src/libstore-tests/local-binary-cache-store.cc +++ b/src/libstore-tests/local-binary-cache-store.cc @@ -1,6 +1,6 @@ #include -#include "local-binary-cache-store.hh" +#include "nix/store/local-binary-cache-store.hh" namespace nix { diff --git a/src/libstore-tests/local-overlay-store.cc b/src/libstore-tests/local-overlay-store.cc index b34ca92375e..fe064c3a51c 100644 --- a/src/libstore-tests/local-overlay-store.cc +++ b/src/libstore-tests/local-overlay-store.cc @@ -3,7 +3,7 @@ #if 0 # include -# include "local-overlay-store.hh" +# include "nix/store/local-overlay-store.hh" namespace nix { diff --git a/src/libstore-tests/local-store.cc b/src/libstore-tests/local-store.cc index abc3ea7963f..ece277609ec 100644 --- a/src/libstore-tests/local-store.cc +++ b/src/libstore-tests/local-store.cc @@ -3,13 +3,13 @@ #if 0 # include -# include "local-store.hh" +# include "nix/store/local-store.hh" // Needed for template specialisations. This is not good! When we // overhaul how store configs work, this should be fixed. -# include "args.hh" -# include "config-impl.hh" -# include "abstract-setting-to-json.hh" +# include "nix/util/args.hh" +# include "nix/util/config-impl.hh" +# include "nix/util/abstract-setting-to-json.hh" namespace nix { diff --git a/src/libstore-tests/machines.cc b/src/libstore-tests/machines.cc index 2d66e953408..f11866e0816 100644 --- a/src/libstore-tests/machines.cc +++ b/src/libstore-tests/machines.cc @@ -1,8 +1,8 @@ -#include "machines.hh" -#include "file-system.hh" -#include "util.hh" +#include "nix/store/machines.hh" +#include "nix/util/file-system.hh" +#include "nix/util/util.hh" -#include "tests/characterization.hh" +#include "nix/util/tests/characterization.hh" #include #include @@ -73,6 +73,32 @@ TEST(machines, getMachinesWithSemicolonSeparator) { EXPECT_THAT(actual, Contains(Field(&Machine::storeUri, AuthorityMatches("nix@itchy.labs.cs.uu.nl")))); } +TEST(machines, getMachinesWithCommentsAndSemicolonSeparator) { + auto actual = Machine::parseConfig({}, + "# This is a comment ; this is still that comment\n" + "nix@scratchy.labs.cs.uu.nl ; nix@itchy.labs.cs.uu.nl\n" + "# This is also a comment ; this also is still that comment\n" + "nix@scabby.labs.cs.uu.nl\n"); + EXPECT_THAT(actual, SizeIs(3)); + EXPECT_THAT(actual, Contains(Field(&Machine::storeUri, AuthorityMatches("nix@scratchy.labs.cs.uu.nl")))); + EXPECT_THAT(actual, Contains(Field(&Machine::storeUri, AuthorityMatches("nix@itchy.labs.cs.uu.nl")))); + EXPECT_THAT(actual, Contains(Field(&Machine::storeUri, AuthorityMatches("nix@scabby.labs.cs.uu.nl")))); +} + +TEST(machines, getMachinesWithFunnyWhitespace) { + auto actual = Machine::parseConfig({}, + " # comment ; comment\n" + " nix@scratchy.labs.cs.uu.nl ; nix@itchy.labs.cs.uu.nl \n" + "\n \n" + "\n ;;; \n" + "\n ; ; \n" + "nix@scabby.labs.cs.uu.nl\n\n"); + EXPECT_THAT(actual, SizeIs(3)); + EXPECT_THAT(actual, Contains(Field(&Machine::storeUri, AuthorityMatches("nix@scratchy.labs.cs.uu.nl")))); + EXPECT_THAT(actual, Contains(Field(&Machine::storeUri, AuthorityMatches("nix@itchy.labs.cs.uu.nl")))); + EXPECT_THAT(actual, Contains(Field(&Machine::storeUri, AuthorityMatches("nix@scabby.labs.cs.uu.nl")))); +} + TEST(machines, getMachinesWithCorrectCompleteSingleBuilder) { auto actual = Machine::parseConfig({}, "nix@scratchy.labs.cs.uu.nl i686-linux " @@ -137,8 +163,8 @@ TEST(machines, getMachinesWithIncorrectFormat) { } TEST(machines, getMachinesWithCorrectFileReference) { - auto path = fs::weakly_canonical(getUnitTestData() / "machines/valid"); - ASSERT_TRUE(fs::exists(path)); + auto path = std::filesystem::weakly_canonical(getUnitTestData() / "machines/valid"); + ASSERT_TRUE(std::filesystem::exists(path)); auto actual = Machine::parseConfig({}, "@" + path.string()); ASSERT_THAT(actual, SizeIs(3)); @@ -148,22 +174,22 @@ TEST(machines, getMachinesWithCorrectFileReference) { } TEST(machines, getMachinesWithCorrectFileReferenceToEmptyFile) { - fs::path path = "/dev/null"; - ASSERT_TRUE(fs::exists(path)); + std::filesystem::path path = "/dev/null"; + ASSERT_TRUE(std::filesystem::exists(path)); auto actual = Machine::parseConfig({}, "@" + path.string()); ASSERT_THAT(actual, SizeIs(0)); } TEST(machines, getMachinesWithIncorrectFileReference) { - auto path = fs::weakly_canonical("/not/a/file"); - ASSERT_TRUE(!fs::exists(path)); + auto path = std::filesystem::weakly_canonical("/not/a/file"); + ASSERT_TRUE(!std::filesystem::exists(path)); auto actual = Machine::parseConfig({}, "@" + path.string()); ASSERT_THAT(actual, SizeIs(0)); } TEST(machines, getMachinesWithCorrectFileReferenceToIncorrectFile) { EXPECT_THROW( - Machine::parseConfig({}, "@" + fs::weakly_canonical(getUnitTestData() / "machines" / "bad_format").string()), + Machine::parseConfig({}, "@" + std::filesystem::weakly_canonical(getUnitTestData() / "machines" / "bad_format").string()), FormatError); } diff --git a/src/libstore-tests/meson.build b/src/libstore-tests/meson.build index fc9152f2fb6..8a1ff40f074 100644 --- a/src/libstore-tests/meson.build +++ b/src/libstore-tests/meson.build @@ -4,8 +4,6 @@ project('nix-store-tests', 'cpp', 'cpp_std=c++2a', # TODO(Qyriad): increase the warning level 'warning_level=1', - 'debug=true', - 'optimization=2', 'errorlogs=true', # Please print logs for tests that fail ], meson_version : '>= 1.1', @@ -14,19 +12,21 @@ project('nix-store-tests', 'cpp', cxx = meson.get_compiler('cpp') -subdir('build-utils-meson/deps-lists') +subdir('nix-meson-build-support/deps-lists') + +nix_store = dependency('nix-store') deps_private_maybe_subproject = [ - dependency('nix-store'), + nix_store, dependency('nix-store-c'), dependency('nix-store-test-support'), ] deps_public_maybe_subproject = [ ] -subdir('build-utils-meson/subprojects') +subdir('nix-meson-build-support/subprojects') -subdir('build-utils-meson/export-all-symbols') -subdir('build-utils-meson/windows-version') +subdir('nix-meson-build-support/export-all-symbols') +subdir('nix-meson-build-support/windows-version') sqlite = dependency('sqlite3', 'sqlite', version : '>=3.6.19') deps_private += sqlite @@ -37,20 +37,20 @@ deps_private += rapidcheck gtest = dependency('gtest', main : true) deps_private += gtest -gtest = dependency('gmock') -deps_private += gtest +configdata = configuration_data() +configdata.set_quoted('PACKAGE_VERSION', meson.project_version()) + +configdata.set_quoted('NIX_STORE_DIR', nix_store.get_variable('storedir')) -add_project_arguments( - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. - '-include', 'config-util.hh', - '-include', 'config-store.hh', - '-include', 'config-util.h', - '-include', 'config-store.h', - language : 'cpp', +config_priv_h = configure_file( + configuration : configdata, + output : 'store-tests-config.hh', ) -subdir('build-utils-meson/common') +gtest = dependency('gmock') +deps_private += gtest + +subdir('nix-meson-build-support/common') sources = files( 'common-protocol.cc', @@ -86,6 +86,7 @@ include_dirs = [include_directories('.')] this_exe = executable( meson.project_name(), sources, + config_priv_h, dependencies : deps_private_subproject + deps_private + deps_other, include_directories : include_dirs, # TODO: -lrapidcheck, see ../libutil-support/build.meson diff --git a/src/libstore-tests/nar-info-disk-cache.cc b/src/libstore-tests/nar-info-disk-cache.cc index b4bdb832957..4c7354c0c1f 100644 --- a/src/libstore-tests/nar-info-disk-cache.cc +++ b/src/libstore-tests/nar-info-disk-cache.cc @@ -1,8 +1,8 @@ -#include "nar-info-disk-cache.hh" +#include "nix/store/nar-info-disk-cache.hh" #include #include -#include "sqlite.hh" +#include "nix/store/sqlite.hh" #include diff --git a/src/libstore-tests/nar-info.cc b/src/libstore-tests/nar-info.cc index 0d155743d91..1979deef81d 100644 --- a/src/libstore-tests/nar-info.cc +++ b/src/libstore-tests/nar-info.cc @@ -1,11 +1,11 @@ #include #include -#include "path-info.hh" -#include "nar-info.hh" +#include "nix/store/path-info.hh" +#include "nix/store/nar-info.hh" -#include "tests/characterization.hh" -#include "tests/libstore.hh" +#include "nix/util/tests/characterization.hh" +#include "nix/store/tests/libstore.hh" namespace nix { diff --git a/src/libstore-tests/nix-meson-build-support b/src/libstore-tests/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/libstore-tests/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/libstore-tests/nix_api_store.cc b/src/libstore-tests/nix_api_store.cc index 7c6ec078057..3d9f7908b3f 100644 --- a/src/libstore-tests/nix_api_store.cc +++ b/src/libstore-tests/nix_api_store.cc @@ -3,8 +3,10 @@ #include "nix_api_store.h" #include "nix_api_store_internal.h" -#include "tests/nix_api_store.hh" -#include "tests/string_callback.hh" +#include "nix/store/tests/nix_api_store.hh" +#include "nix/util/tests/string_callback.hh" + +#include "store-tests-config.hh" namespace nixC { @@ -24,6 +26,39 @@ TEST_F(nix_api_store_test, nix_store_get_uri) ASSERT_STREQ("local", str.c_str()); } +TEST_F(nix_api_util_context, nix_store_get_storedir_default) +{ + if (nix::getEnv("HOME").value_or("") == "/homeless-shelter") { + // skipping test in sandbox because nix_store_open tries to create /nix/var/nix/profiles + GTEST_SKIP(); + } + nix_libstore_init(ctx); + Store * store = nix_store_open(ctx, nullptr, nullptr); + assert_ctx_ok(); + ASSERT_NE(store, nullptr); + + std::string str; + auto ret = nix_store_get_storedir(ctx, store, OBSERVE_STRING(str)); + assert_ctx_ok(); + ASSERT_EQ(NIX_OK, ret); + + // These tests run with a unique storeDir, but not a relocated store + ASSERT_STREQ(NIX_STORE_DIR, str.c_str()); + + nix_store_free(store); +} + +TEST_F(nix_api_store_test, nix_store_get_storedir) +{ + std::string str; + auto ret = nix_store_get_storedir(ctx, store, OBSERVE_STRING(str)); + assert_ctx_ok(); + ASSERT_EQ(NIX_OK, ret); + + // These tests run with a unique storeDir, but not a relocated store + ASSERT_STREQ(nixStoreDir.c_str(), str.c_str()); +} + TEST_F(nix_api_store_test, InvalidPathFails) { nix_store_parse_path(ctx, store, "invalid-path"); @@ -36,17 +71,21 @@ TEST_F(nix_api_store_test, ReturnsValidStorePath) ASSERT_NE(result, nullptr); ASSERT_STREQ("name", result->path.name().data()); ASSERT_STREQ(PATH_SUFFIX.substr(1).c_str(), result->path.to_string().data()); + nix_store_path_free(result); } TEST_F(nix_api_store_test, SetsLastErrCodeToNixOk) { - nix_store_parse_path(ctx, store, (nixStoreDir + PATH_SUFFIX).c_str()); + StorePath * path = nix_store_parse_path(ctx, store, (nixStoreDir + PATH_SUFFIX).c_str()); ASSERT_EQ(ctx->last_err_code, NIX_OK); + nix_store_path_free(path); } TEST_F(nix_api_store_test, DoesNotCrashWhenContextIsNull) { - ASSERT_NO_THROW(nix_store_parse_path(ctx, store, (nixStoreDir + PATH_SUFFIX).c_str())); + StorePath * path = nullptr; + ASSERT_NO_THROW(path = nix_store_parse_path(ctx, store, (nixStoreDir + PATH_SUFFIX).c_str())); + nix_store_path_free(path); } TEST_F(nix_api_store_test, get_version) @@ -84,6 +123,87 @@ TEST_F(nix_api_store_test, nix_store_is_valid_path_not_in_store) { StorePath * path = nix_store_parse_path(ctx, store, (nixStoreDir + PATH_SUFFIX).c_str()); ASSERT_EQ(false, nix_store_is_valid_path(ctx, store, path)); + nix_store_path_free(path); } +TEST_F(nix_api_store_test, nix_store_real_path) +{ + StorePath * path = nix_store_parse_path(ctx, store, (nixStoreDir + PATH_SUFFIX).c_str()); + std::string rp; + auto ret = nix_store_real_path(ctx, store, path, OBSERVE_STRING(rp)); + assert_ctx_ok(); + ASSERT_EQ(NIX_OK, ret); + // Assumption: we're not testing with a relocated store + ASSERT_STREQ((nixStoreDir + PATH_SUFFIX).c_str(), rp.c_str()); + + nix_store_path_free(path); +} + +TEST_F(nix_api_util_context, nix_store_real_path_relocated) +{ + if (nix::getEnv("HOME").value_or("") == "/homeless-shelter") { + // Can't open default store from within sandbox + GTEST_SKIP(); + } + auto tmp = nix::createTempDir(); + std::string storeRoot = tmp + "/store"; + std::string stateDir = tmp + "/state"; + std::string logDir = tmp + "/log"; + const char * rootkv[] = {"root", storeRoot.c_str()}; + const char * statekv[] = {"state", stateDir.c_str()}; + const char * logkv[] = {"log", logDir.c_str()}; + // const char * rokv[] = {"read-only", "true"}; + const char ** kvs[] = {rootkv, statekv, logkv, NULL}; + + nix_libstore_init(ctx); + assert_ctx_ok(); + + Store * store = nix_store_open(ctx, "local", kvs); + assert_ctx_ok(); + ASSERT_NE(store, nullptr); + + std::string nixStoreDir; + auto ret = nix_store_get_storedir(ctx, store, OBSERVE_STRING(nixStoreDir)); + ASSERT_EQ(NIX_OK, ret); + ASSERT_STREQ(NIX_STORE_DIR, nixStoreDir.c_str()); + + StorePath * path = nix_store_parse_path(ctx, store, (nixStoreDir + PATH_SUFFIX).c_str()); + assert_ctx_ok(); + ASSERT_NE(path, nullptr); + + std::string rp; + ret = nix_store_real_path(ctx, store, path, OBSERVE_STRING(rp)); + assert_ctx_ok(); + ASSERT_EQ(NIX_OK, ret); + + // Assumption: we're not testing with a relocated store + ASSERT_STREQ((storeRoot + NIX_STORE_DIR + PATH_SUFFIX).c_str(), rp.c_str()); + + nix_store_path_free(path); } + +TEST_F(nix_api_util_context, nix_store_real_path_binary_cache) +{ + if (nix::getEnv("HOME").value_or("") == "/homeless-shelter") { + // TODO: override NIX_CACHE_HOME? + // skipping test in sandbox because narinfo cache can't be written + GTEST_SKIP(); + } + + Store * store = nix_store_open(ctx, "https://cache.nixos.org", nullptr); + assert_ctx_ok(); + ASSERT_NE(store, nullptr); + + std::string path_raw = std::string(NIX_STORE_DIR) + PATH_SUFFIX; + StorePath * path = nix_store_parse_path(ctx, store, path_raw.c_str()); + assert_ctx_ok(); + ASSERT_NE(path, nullptr); + + std::string rp; + auto ret = nix_store_real_path(ctx, store, path, OBSERVE_STRING(rp)); + assert_ctx_ok(); + ASSERT_EQ(NIX_OK, ret); + ASSERT_STREQ(path_raw.c_str(), rp.c_str()); +} + +} // namespace nixC diff --git a/src/libstore-tests/outputs-spec.cc b/src/libstore-tests/outputs-spec.cc index 63cde681bbf..12f285e0d05 100644 --- a/src/libstore-tests/outputs-spec.cc +++ b/src/libstore-tests/outputs-spec.cc @@ -1,4 +1,4 @@ -#include "tests/outputs-spec.hh" +#include "nix/store/tests/outputs-spec.hh" #include #include @@ -7,7 +7,7 @@ namespace nix { TEST(OutputsSpec, no_empty_names) { - ASSERT_DEATH(OutputsSpec::Names { std::set { } }, ""); + ASSERT_DEATH(OutputsSpec::Names { StringSet { } }, ""); } #define TEST_DONT_PARSE(NAME, STR) \ @@ -46,7 +46,7 @@ TEST(OutputsSpec, names_underscore) { ASSERT_EQ(expected.to_string(), str); } -TEST(OutputsSpec, names_numberic) { +TEST(OutputsSpec, names_numeric) { std::string_view str = "01"; OutputsSpec expected = OutputsSpec::Names { "01" }; ASSERT_EQ(OutputsSpec::parse(str), expected); @@ -126,7 +126,7 @@ TEST_DONT_PARSE(star_second, "^foo,*") #undef TEST_DONT_PARSE -TEST(ExtendedOutputsSpec, defeault) { +TEST(ExtendedOutputsSpec, default) { std::string_view str = "foo"; auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse(str); ASSERT_EQ(prefix, "foo"); diff --git a/src/libstore-tests/package.nix b/src/libstore-tests/package.nix index 3704d8c5ce8..b39ee7fa73c 100644 --- a/src/libstore-tests/package.nix +++ b/src/libstore-tests/package.nix @@ -1,21 +1,22 @@ -{ lib -, buildPackages -, stdenv -, mkMesonExecutable +{ + lib, + buildPackages, + stdenv, + mkMesonExecutable, -, nix-store -, nix-store-c -, nix-store-test-support -, sqlite + nix-store, + nix-store-c, + nix-store-test-support, + sqlite, -, rapidcheck -, gtest -, runCommand + rapidcheck, + gtest, + runCommand, -# Configuration Options + # Configuration Options -, version -, filesetToSource + version, + filesetToSource, }: let @@ -28,8 +29,8 @@ mkMesonExecutable (finalAttrs: { workDir = ./.; fileset = fileset.unions [ - ../../build-utils-meson - ./build-utils-meson + ../../nix-meson-build-support + ./nix-meson-build-support ../../.version ./.version ./meson.build @@ -51,43 +52,38 @@ mkMesonExecutable (finalAttrs: { nix-store-test-support ]; - preConfigure = - # "Inline" .version so it's not a symlink, and includes the suffix. - # Do the meson utils, without modification. - '' - chmod u+w ./.version - echo ${version} > ../../.version - ''; - mesonFlags = [ ]; - env = lib.optionalAttrs (stdenv.isLinux && !(stdenv.hostPlatform.isStatic && stdenv.system == "aarch64-linux")) { - LDFLAGS = "-fuse-ld=gold"; - }; - passthru = { tests = { - run = let - # Some data is shared with the functional tests: they create it, - # we consume it. - data = filesetToSource { - root = ../..; - fileset = lib.fileset.unions [ - ./data - ../../tests/functional/derivation - ]; - }; - in runCommand "${finalAttrs.pname}-run" { - meta.broken = !stdenv.hostPlatform.emulatorAvailable buildPackages; - } (lib.optionalString stdenv.hostPlatform.isWindows '' - export HOME="$PWD/home-dir" - mkdir -p "$HOME" - '' + '' - export _NIX_TEST_UNIT_DATA=${data + "/src/libstore-tests/data"} - ${stdenv.hostPlatform.emulator buildPackages} ${lib.getExe finalAttrs.finalPackage} - touch $out - ''); + run = + let + # Some data is shared with the functional tests: they create it, + # we consume it. + data = filesetToSource { + root = ../..; + fileset = lib.fileset.unions [ + ./data + ../../tests/functional/derivation + ]; + }; + in + runCommand "${finalAttrs.pname}-run" + { + meta.broken = !stdenv.hostPlatform.emulatorAvailable buildPackages; + } + ( + lib.optionalString stdenv.hostPlatform.isWindows '' + export HOME="$PWD/home-dir" + mkdir -p "$HOME" + '' + + '' + export _NIX_TEST_UNIT_DATA=${data + "/src/libstore-tests/data"} + ${stdenv.hostPlatform.emulator buildPackages} ${lib.getExe finalAttrs.finalPackage} + touch $out + '' + ); }; }; diff --git a/src/libstore-tests/path-info.cc b/src/libstore-tests/path-info.cc index d6c4c2a7f7e..a7699f7adb9 100644 --- a/src/libstore-tests/path-info.cc +++ b/src/libstore-tests/path-info.cc @@ -1,10 +1,10 @@ #include #include -#include "path-info.hh" +#include "nix/store/path-info.hh" -#include "tests/characterization.hh" -#include "tests/libstore.hh" +#include "nix/util/tests/characterization.hh" +#include "nix/store/tests/libstore.hh" namespace nix { @@ -93,7 +93,7 @@ TEST_F(PathInfoTest, PathInfo_full_shortRefs) { ValidPathInfo it = makeFullKeyed(*store, true); // it.references = unkeyed.references; auto refs = it.shortRefs(); - ASSERT_EQ(refs.size(), 2); + ASSERT_EQ(refs.size(), 2u); ASSERT_EQ(*refs.begin(), "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar"); ASSERT_EQ(*++refs.begin(), "n5wkd9frr45pa74if5gpz9j7mifg27fh-foo"); } diff --git a/src/libstore-tests/path.cc b/src/libstore-tests/path.cc index c4c055abf0c..4da73a0ad6c 100644 --- a/src/libstore-tests/path.cc +++ b/src/libstore-tests/path.cc @@ -4,12 +4,12 @@ #include #include -#include "path-regex.hh" -#include "store-api.hh" +#include "nix/store/path-regex.hh" +#include "nix/store/store-api.hh" -#include "tests/hash.hh" -#include "tests/libstore.hh" -#include "tests/path.hh" +#include "nix/util/tests/hash.hh" +#include "nix/store/tests/libstore.hh" +#include "nix/store/tests/path.hh" namespace nix { diff --git a/src/libstore-tests/references.cc b/src/libstore-tests/references.cc index d91d1cedd65..59993727d77 100644 --- a/src/libstore-tests/references.cc +++ b/src/libstore-tests/references.cc @@ -1,4 +1,4 @@ -#include "references.hh" +#include "nix/util/references.hh" #include diff --git a/src/libstore-tests/s3-binary-cache-store.cc b/src/libstore-tests/s3-binary-cache-store.cc index 7aa5f2f2c06..251e96172b6 100644 --- a/src/libstore-tests/s3-binary-cache-store.cc +++ b/src/libstore-tests/s3-binary-cache-store.cc @@ -1,8 +1,8 @@ -#if ENABLE_S3 +#include "nix/store/s3-binary-cache-store.hh" -# include +#if NIX_WITH_S3_SUPPORT -# include "s3-binary-cache-store.hh" +# include namespace nix { diff --git a/src/libstore-tests/serve-protocol.cc b/src/libstore-tests/serve-protocol.cc index 3dbbf38799a..69dab5488b4 100644 --- a/src/libstore-tests/serve-protocol.cc +++ b/src/libstore-tests/serve-protocol.cc @@ -4,13 +4,13 @@ #include #include -#include "serve-protocol.hh" -#include "serve-protocol-impl.hh" -#include "serve-protocol-connection.hh" -#include "build-result.hh" -#include "file-descriptor.hh" -#include "tests/protocol.hh" -#include "tests/characterization.hh" +#include "nix/store/serve-protocol.hh" +#include "nix/store/serve-protocol-impl.hh" +#include "nix/store/serve-protocol-connection.hh" +#include "nix/store/build-result.hh" +#include "nix/util/file-descriptor.hh" +#include "nix/store/tests/protocol.hh" +#include "nix/util/tests/characterization.hh" namespace nix { @@ -374,7 +374,7 @@ VERSIONED_CHARACTERIZATION_TEST( set, "set", defaultVersion, - (std::tuple, std::set, std::set, std::set>> { + (std::tuple> { { }, { "" }, { "", "foo", "bar" }, diff --git a/src/libstore-tests/ssh-store.cc b/src/libstore-tests/ssh-store.cc index b853a5f1fb9..ccb87b767a9 100644 --- a/src/libstore-tests/ssh-store.cc +++ b/src/libstore-tests/ssh-store.cc @@ -3,7 +3,7 @@ #if 0 # include -# include "ssh-store.hh" +# include "nix/store/ssh-store.hh" namespace nix { diff --git a/src/libstore-tests/store-reference.cc b/src/libstore-tests/store-reference.cc index d4c42f0fda1..dd1b8309072 100644 --- a/src/libstore-tests/store-reference.cc +++ b/src/libstore-tests/store-reference.cc @@ -1,11 +1,11 @@ #include #include -#include "file-system.hh" -#include "store-reference.hh" +#include "nix/util/file-system.hh" +#include "nix/store/store-reference.hh" -#include "tests/characterization.hh" -#include "tests/libstore.hh" +#include "nix/util/tests/characterization.hh" +#include "nix/store/tests/libstore.hh" namespace nix { diff --git a/src/libstore-tests/uds-remote-store.cc b/src/libstore-tests/uds-remote-store.cc index 5ccb208714f..c6a92666831 100644 --- a/src/libstore-tests/uds-remote-store.cc +++ b/src/libstore-tests/uds-remote-store.cc @@ -3,7 +3,7 @@ #if 0 # include -# include "uds-remote-store.hh" +# include "nix/store/uds-remote-store.hh" namespace nix { diff --git a/src/libstore-tests/worker-protocol.cc b/src/libstore-tests/worker-protocol.cc index 99b042d5ba4..4baf8a325ee 100644 --- a/src/libstore-tests/worker-protocol.cc +++ b/src/libstore-tests/worker-protocol.cc @@ -4,13 +4,13 @@ #include #include -#include "worker-protocol.hh" -#include "worker-protocol-connection.hh" -#include "worker-protocol-impl.hh" -#include "derived-path.hh" -#include "build-result.hh" -#include "tests/protocol.hh" -#include "tests/characterization.hh" +#include "nix/store/worker-protocol.hh" +#include "nix/store/worker-protocol-connection.hh" +#include "nix/store/worker-protocol-impl.hh" +#include "nix/store/derived-path.hh" +#include "nix/store/build-result.hh" +#include "nix/store/tests/protocol.hh" +#include "nix/util/tests/characterization.hh" namespace nix { @@ -574,7 +574,7 @@ VERSIONED_CHARACTERIZATION_TEST( set, "set", defaultVersion, - (std::tuple, std::set, std::set, std::set>> { + (std::tuple> { { }, { "" }, { "", "foo", "bar" }, @@ -685,7 +685,7 @@ TEST_F(WorkerProtoTest, handshake_features) toClient.create(); toServer.create(); - std::tuple> clientResult; + std::tuple clientResult; auto clientThread = std::thread([&]() { FdSink out { toServer.writeSide.get() }; @@ -702,8 +702,8 @@ TEST_F(WorkerProtoTest, handshake_features) clientThread.join(); EXPECT_EQ(clientResult, daemonResult); - EXPECT_EQ(std::get<0>(clientResult), 123); - EXPECT_EQ(std::get<1>(clientResult), std::set({"bar", "xyzzy"})); + EXPECT_EQ(std::get<0>(clientResult), 123u); + EXPECT_EQ(std::get<1>(clientResult), WorkerProto::FeatureSet({"bar", "xyzzy"})); } /// Has to be a `BufferedSink` for handshake. diff --git a/src/libstore/binary-cache-store.cc b/src/libstore/binary-cache-store.cc index e8c8892b337..4df9651f03f 100644 --- a/src/libstore/binary-cache-store.cc +++ b/src/libstore/binary-cache-store.cc @@ -1,18 +1,18 @@ -#include "archive.hh" -#include "binary-cache-store.hh" -#include "compression.hh" -#include "derivations.hh" -#include "source-accessor.hh" -#include "globals.hh" -#include "nar-info.hh" -#include "sync.hh" -#include "remote-fs-accessor.hh" -#include "nar-info-disk-cache.hh" -#include "nar-accessor.hh" -#include "thread-pool.hh" -#include "callback.hh" -#include "signals.hh" -#include "archive.hh" +#include "nix/util/archive.hh" +#include "nix/store/binary-cache-store.hh" +#include "nix/util/compression.hh" +#include "nix/store/derivations.hh" +#include "nix/util/source-accessor.hh" +#include "nix/store/globals.hh" +#include "nix/store/nar-info.hh" +#include "nix/util/sync.hh" +#include "nix/store/remote-fs-accessor.hh" +#include "nix/store/nar-info-disk-cache.hh" +#include "nix/store/nar-accessor.hh" +#include "nix/util/thread-pool.hh" +#include "nix/util/callback.hh" +#include "nix/util/signals.hh" +#include "nix/util/archive.hh" #include #include @@ -24,13 +24,21 @@ namespace nix { -BinaryCacheStore::BinaryCacheStore(const Params & params) - : BinaryCacheStoreConfig(params) - , Store(params) +BinaryCacheStore::BinaryCacheStore(Config & config) + : config{config} { - if (secretKeyFile != "") - signer = std::make_unique( - SecretKey { readFile(secretKeyFile) }); + if (config.secretKeyFile != "") + signers.push_back(std::make_unique( + SecretKey { readFile(config.secretKeyFile) })); + + if (config.secretKeyFiles != "") { + std::stringstream ss(config.secretKeyFiles); + Path keyPath; + while (std::getline(ss, keyPath, ',')) { + signers.push_back(std::make_unique( + SecretKey { readFile(keyPath) })); + } + } StringSink sink; sink << narVersionMagic1; @@ -39,15 +47,13 @@ BinaryCacheStore::BinaryCacheStore(const Params & params) void BinaryCacheStore::init() { - std::string cacheInfoFile = "nix-cache-info"; - - auto cacheInfo = getFile(cacheInfoFile); + auto cacheInfo = getNixCacheInfo(); if (!cacheInfo) { upsertFile(cacheInfoFile, "StoreDir: " + storeDir + "\n", "text/x-nix-cache-info"); } else { for (auto & line : tokenizeString(*cacheInfo, "\n")) { - size_t colon= line.find(':'); - if (colon ==std::string::npos) continue; + size_t colon = line.find(':'); + if (colon == std::string::npos) continue; auto name = line.substr(0, colon); auto value = trim(line.substr(colon + 1, std::string::npos)); if (name == "StoreDir") { @@ -55,14 +61,19 @@ void BinaryCacheStore::init() throw Error("binary cache '%s' is for Nix stores with prefix '%s', not '%s'", getUri(), value, storeDir); } else if (name == "WantMassQuery") { - wantMassQuery.setDefault(value == "1"); + config.wantMassQuery.setDefault(value == "1"); } else if (name == "Priority") { - priority.setDefault(std::stoi(value)); + config.priority.setDefault(std::stoi(value)); } } } } +std::optional BinaryCacheStore::getNixCacheInfo() +{ + return getFile(cacheInfoFile); +} + void BinaryCacheStore::upsertFile(const std::string & path, std::string && data, const std::string & mimeType) @@ -144,7 +155,11 @@ ref BinaryCacheStore::addToStoreCommon( { FdSink fileSink(fdTemp.get()); TeeSink teeSinkCompressed { fileSink, fileHashSink }; - auto compressionSink = makeCompressionSink(compression, teeSinkCompressed, parallelCompression, compressionLevel); + auto compressionSink = makeCompressionSink( + config.compression, + teeSinkCompressed, + config.parallelCompression, + config.compressionLevel); TeeSink teeSinkUncompressed { *compressionSink, narHashSink }; TeeSource teeSource { narSource, teeSinkUncompressed }; narAccessor = makeNarAccessor(teeSource); @@ -156,17 +171,17 @@ ref BinaryCacheStore::addToStoreCommon( auto info = mkInfo(narHashSink.finish()); auto narInfo = make_ref(info); - narInfo->compression = compression; + narInfo->compression = config.compression; auto [fileHash, fileSize] = fileHashSink.finish(); narInfo->fileHash = fileHash; narInfo->fileSize = fileSize; narInfo->url = "nar/" + narInfo->fileHash->to_string(HashFormat::Nix32, false) + ".nar" - + (compression == "xz" ? ".xz" : - compression == "bzip2" ? ".bz2" : - compression == "zstd" ? ".zst" : - compression == "lzip" ? ".lzip" : - compression == "lz4" ? ".lz4" : - compression == "br" ? ".br" : + + (config.compression == "xz" ? ".xz" : + config.compression == "bzip2" ? ".bz2" : + config.compression == "zstd" ? ".zst" : + config.compression == "lzip" ? ".lzip" : + config.compression == "lz4" ? ".lz4" : + config.compression == "br" ? ".br" : ""); auto duration = std::chrono::duration_cast(now2 - now1).count(); @@ -188,7 +203,7 @@ ref BinaryCacheStore::addToStoreCommon( /* Optionally write a JSON file containing a listing of the contents of the NAR. */ - if (writeNARListing) { + if (config.writeNARListing) { nlohmann::json j = { {"version", 1}, {"root", listNar(ref(narAccessor), CanonPath::root, true)}, @@ -200,7 +215,7 @@ ref BinaryCacheStore::addToStoreCommon( /* Optionally maintain an index of DWARF debug info files consisting of JSON files named 'debuginfo/' that specify the NAR file and member containing the debug info. */ - if (writeDebugInfo) { + if (config.writeDebugInfo) { CanonPath buildIdDir("lib/debug/.build-id"); @@ -267,9 +282,9 @@ ref BinaryCacheStore::addToStoreCommon( stats.narWriteCompressedBytes += fileSize; stats.narWriteCompressionTimeMs += duration; - /* Atomically write the NAR info file.*/ - if (signer) narInfo->sign(*this, *signer); + narInfo->sign(*this, signers); + /* Atomically write the NAR info file.*/ writeNarInfo(narInfo); stats.narInfoWrite++; @@ -512,7 +527,7 @@ void BinaryCacheStore::registerDrvOutput(const Realisation& info) { ref BinaryCacheStore::getFSAccessor(bool requireValidPath) { - return make_ref(ref(shared_from_this()), requireValidPath, localNarCache); + return make_ref(ref(shared_from_this()), requireValidPath, config.localNarCache); } void BinaryCacheStore::addSignatures(const StorePath & storePath, const StringSet & sigs) diff --git a/src/libstore/build-result.cc b/src/libstore/build-result.cc index 96cbfd62fff..09166133786 100644 --- a/src/libstore/build-result.cc +++ b/src/libstore/build-result.cc @@ -1,4 +1,4 @@ -#include "build-result.hh" +#include "nix/store/build-result.hh" namespace nix { diff --git a/src/libstore/build-utils-meson b/src/libstore/build-utils-meson deleted file mode 120000 index 5fff21bab55..00000000000 --- a/src/libstore/build-utils-meson +++ /dev/null @@ -1 +0,0 @@ -../../build-utils-meson \ No newline at end of file diff --git a/src/libstore/build/derivation-building-goal.cc b/src/libstore/build/derivation-building-goal.cc new file mode 100644 index 00000000000..3019d9d728a --- /dev/null +++ b/src/libstore/build/derivation-building-goal.cc @@ -0,0 +1,1248 @@ +#include "nix/store/build/derivation-building-goal.hh" +#include "nix/store/build/derivation-trampoline-goal.hh" +#ifndef _WIN32 // TODO enable build hook on Windows +# include "nix/store/build/hook-instance.hh" +# include "nix/store/build/derivation-builder.hh" +#endif +#include "nix/util/processes.hh" +#include "nix/util/config-global.hh" +#include "nix/store/build/worker.hh" +#include "nix/util/util.hh" +#include "nix/util/compression.hh" +#include "nix/store/common-protocol.hh" +#include "nix/store/common-protocol-impl.hh" +#include "nix/store/local-store.hh" // TODO remove, along with remaining downcasts + +#include +#include +#include +#include + +#include + +#include "nix/util/strings.hh" + +namespace nix { + +DerivationBuildingGoal::DerivationBuildingGoal(const StorePath & drvPath, const Derivation & drv_, + Worker & worker, BuildMode buildMode) + : Goal(worker, gaveUpOnSubstitution()) + , drvPath(drvPath) + , buildMode(buildMode) +{ + drv = std::make_unique(drv_); + + if (auto parsedOpt = StructuredAttrs::tryParse(drv->env)) { + parsedDrv = std::make_unique(*parsedOpt); + } + try { + drvOptions = std::make_unique( + DerivationOptions::fromStructuredAttrs(drv->env, parsedDrv.get())); + } catch (Error & e) { + e.addTrace({}, "while parsing derivation '%s'", worker.store.printStorePath(drvPath)); + throw; + } + + name = fmt("building of '%s' from in-memory derivation", worker.store.printStorePath(drvPath)); + trace("created"); + + /* Prevent the .chroot directory from being + garbage-collected. (See isActiveTempFile() in gc.cc.) */ + worker.store.addTempRoot(this->drvPath); +} + + +DerivationBuildingGoal::~DerivationBuildingGoal() +{ + /* Careful: we should never ever throw an exception from a + destructor. */ + try { killChild(); } catch (...) { ignoreExceptionInDestructor(); } +#ifndef _WIN32 // TODO enable `DerivationBuilder` on Windows + if (builder) { + try { builder->stopDaemon(); } catch (...) { ignoreExceptionInDestructor(); } + try { builder->deleteTmpDir(false); } catch (...) { ignoreExceptionInDestructor(); } + } +#endif + try { closeLogFile(); } catch (...) { ignoreExceptionInDestructor(); } +} + + +std::string DerivationBuildingGoal::key() +{ + /* Ensure that derivations get built in order of their name, + i.e. a derivation named "aardvark" always comes before + "baboon". And substitution goals always happen before + derivation goals (due to "bd$"). */ + return "bd$" + std::string(drvPath.name()) + "$" + worker.store.printStorePath(drvPath); +} + + +void DerivationBuildingGoal::killChild() +{ +#ifndef _WIN32 // TODO enable build hook on Windows + hook.reset(); +#endif +#ifndef _WIN32 // TODO enable `DerivationBuilder` on Windows + if (builder && builder->pid != -1) { + worker.childTerminated(this); + + // FIXME: move this into DerivationBuilder. + + /* If we're using a build user, then there is a tricky race + condition: if we kill the build user before the child has + done its setuid() to the build user uid, then it won't be + killed, and we'll potentially lock up in pid.wait(). So + also send a conventional kill to the child. */ + ::kill(-builder->pid, SIGKILL); /* ignore the result */ + + builder->killSandbox(true); + + builder->pid.wait(); + } +#endif +} + + +void DerivationBuildingGoal::timedOut(Error && ex) +{ + killChild(); + // We're not inside a coroutine, hence we can't use co_return here. + // Thus we ignore the return value. + [[maybe_unused]] Done _ = done(BuildResult::TimedOut, {}, std::move(ex)); +} + + +/** + * Used for `inputGoals` local variable below + */ +struct value_comparison +{ + template + bool operator()(const ref & lhs, const ref & rhs) const { + return *lhs < *rhs; + } +}; + + +std::string showKnownOutputs(Store & store, const Derivation & drv) +{ + std::string msg; + StorePathSet expectedOutputPaths; + for (auto & i : drv.outputsAndOptPaths(store)) + if (i.second.second) + expectedOutputPaths.insert(*i.second.second); + if (!expectedOutputPaths.empty()) { + msg += "\nOutput paths:"; + for (auto & p : expectedOutputPaths) + msg += fmt("\n %s", Magenta(store.printStorePath(p))); + } + return msg; +} + + +/* At least one of the output paths could not be + produced using a substitute. So we have to build instead. */ +Goal::Co DerivationBuildingGoal::gaveUpOnSubstitution() +{ + Goals waitees; + + std::map, GoalPtr, value_comparison> inputGoals; + + { + std::function, const DerivedPathMap::ChildNode &)> addWaiteeDerivedPath; + + addWaiteeDerivedPath = [&](ref inputDrv, const DerivedPathMap::ChildNode & inputNode) { + if (!inputNode.value.empty()) { + auto g = worker.makeGoal( + DerivedPath::Built { + .drvPath = inputDrv, + .outputs = inputNode.value, + }, + buildMode == bmRepair ? bmRepair : bmNormal); + inputGoals.insert_or_assign(inputDrv, g); + waitees.insert(std::move(g)); + } + for (const auto & [outputName, childNode] : inputNode.childMap) + addWaiteeDerivedPath( + make_ref(SingleDerivedPath::Built { inputDrv, outputName }), + childNode); + }; + + for (const auto & [inputDrvPath, inputNode] : drv->inputDrvs.map) { + /* Ensure that pure, non-fixed-output derivations don't + depend on impure derivations. */ + if (experimentalFeatureSettings.isEnabled(Xp::ImpureDerivations) && !drv->type().isImpure() && !drv->type().isFixed()) { + auto inputDrv = worker.evalStore.readDerivation(inputDrvPath); + if (inputDrv.type().isImpure()) + throw Error("pure derivation '%s' depends on impure derivation '%s'", + worker.store.printStorePath(drvPath), + worker.store.printStorePath(inputDrvPath)); + } + + addWaiteeDerivedPath(makeConstantStorePathRef(inputDrvPath), inputNode); + } + } + + /* Copy the input sources from the eval store to the build + store. + + Note that some inputs might not be in the eval store because they + are (resolved) derivation outputs in a resolved derivation. */ + if (&worker.evalStore != &worker.store) { + RealisedPath::Set inputSrcs; + for (auto & i : drv->inputSrcs) + if (worker.evalStore.isValidPath(i)) + inputSrcs.insert(i); + copyClosure(worker.evalStore, worker.store, inputSrcs); + } + + for (auto & i : drv->inputSrcs) { + if (worker.store.isValidPath(i)) continue; + if (!settings.useSubstitutes) + throw Error("dependency '%s' of '%s' does not exist, and substitution is disabled", + worker.store.printStorePath(i), worker.store.printStorePath(drvPath)); + waitees.insert(upcast_goal(worker.makePathSubstitutionGoal(i))); + } + + co_await await(std::move(waitees)); + + + trace("all inputs realised"); + + if (nrFailed != 0) { + auto msg = fmt( + "Cannot build '%s'.\n" + "Reason: " ANSI_RED "%d %s failed" ANSI_NORMAL ".", + Magenta(worker.store.printStorePath(drvPath)), + nrFailed, + nrFailed == 1 ? "dependency" : "dependencies"); + msg += showKnownOutputs(worker.store, *drv); + co_return done(BuildResult::DependencyFailed, {}, Error(msg)); + } + + /* Gather information necessary for computing the closure and/or + running the build hook. */ + + /* Determine the full set of input paths. */ + + /* First, the input derivations. */ + { + auto & fullDrv = *drv; + + auto drvType = fullDrv.type(); + bool resolveDrv = std::visit(overloaded { + [&](const DerivationType::InputAddressed & ia) { + /* must resolve if deferred. */ + return ia.deferred; + }, + [&](const DerivationType::ContentAddressed & ca) { + return !fullDrv.inputDrvs.map.empty() && ( + ca.fixed + /* Can optionally resolve if fixed, which is good + for avoiding unnecessary rebuilds. */ + ? experimentalFeatureSettings.isEnabled(Xp::CaDerivations) + /* Must resolve if floating and there are any inputs + drvs. */ + : true); + }, + [&](const DerivationType::Impure &) { + return true; + } + }, drvType.raw) + /* no inputs are outputs of dynamic derivations */ + || std::ranges::any_of( + fullDrv.inputDrvs.map.begin(), + fullDrv.inputDrvs.map.end(), + [](auto & pair) { return !pair.second.childMap.empty(); }); + + if (resolveDrv && !fullDrv.inputDrvs.map.empty()) { + experimentalFeatureSettings.require(Xp::CaDerivations); + + /* We are be able to resolve this derivation based on the + now-known results of dependencies. If so, we become a + stub goal aliasing that resolved derivation goal. */ + std::optional attempt = fullDrv.tryResolve(worker.store, + [&](ref drvPath, const std::string & outputName) -> std::optional { + auto mEntry = get(inputGoals, drvPath); + if (!mEntry) return std::nullopt; + + auto & buildResult = (*mEntry)->buildResult; + if (!buildResult.success()) return std::nullopt; + + auto i = get(buildResult.builtOutputs, outputName); + if (!i) return std::nullopt; + + return i->outPath; + }); + if (!attempt) { + /* TODO (impure derivations-induced tech debt) (see below): + The above attempt should have found it, but because we manage + inputDrvOutputs statefully, sometimes it gets out of sync with + the real source of truth (store). So we query the store + directly if there's a problem. */ + attempt = fullDrv.tryResolve(worker.store, &worker.evalStore); + } + assert(attempt); + Derivation drvResolved { std::move(*attempt) }; + + auto pathResolved = writeDerivation(worker.store, drvResolved); + + auto msg = fmt("resolved derivation: '%s' -> '%s'", + worker.store.printStorePath(drvPath), + worker.store.printStorePath(pathResolved)); + act = std::make_unique(*logger, lvlInfo, actBuildWaiting, msg, + Logger::Fields { + worker.store.printStorePath(drvPath), + worker.store.printStorePath(pathResolved), + }); + + /* TODO https://github.com/NixOS/nix/issues/13247 we should + let the calling goal do this, so it has a change to pass + just the output(s) it cares about. */ + auto resolvedDrvGoal = worker.makeDerivationTrampolineGoal( + pathResolved, OutputsSpec::All{}, drvResolved, buildMode); + { + Goals waitees{resolvedDrvGoal}; + co_await await(std::move(waitees)); + } + + trace("resolved derivation finished"); + + auto resolvedResult = resolvedDrvGoal->buildResult; + + SingleDrvOutputs builtOutputs; + + if (resolvedResult.success()) { + auto resolvedHashes = staticOutputHashes(worker.store, drvResolved); + + StorePathSet outputPaths; + + for (auto & outputName : drvResolved.outputNames()) { + auto initialOutput = get(initialOutputs, outputName); + auto resolvedHash = get(resolvedHashes, outputName); + if ((!initialOutput) || (!resolvedHash)) + throw Error( + "derivation '%s' doesn't have expected output '%s' (derivation-goal.cc/resolve)", + worker.store.printStorePath(drvPath), outputName); + + auto realisation = [&]{ + auto take1 = get(resolvedResult.builtOutputs, outputName); + if (take1) return *take1; + + /* The above `get` should work. But stateful tracking of + outputs in resolvedResult, this can get out of sync with the + store, which is our actual source of truth. For now we just + check the store directly if it fails. */ + auto take2 = worker.evalStore.queryRealisation(DrvOutput { *resolvedHash, outputName }); + if (take2) return *take2; + + throw Error( + "derivation '%s' doesn't have expected output '%s' (derivation-goal.cc/realisation)", + worker.store.printStorePath(pathResolved), outputName); + }(); + + if (!drv->type().isImpure()) { + auto newRealisation = realisation; + newRealisation.id = DrvOutput { initialOutput->outputHash, outputName }; + newRealisation.signatures.clear(); + if (!drv->type().isFixed()) { + auto & drvStore = worker.evalStore.isValidPath(drvPath) + ? worker.evalStore + : worker.store; + newRealisation.dependentRealisations = drvOutputReferences(worker.store, *drv, realisation.outPath, &drvStore); + } + worker.store.signRealisation(newRealisation); + worker.store.registerDrvOutput(newRealisation); + } + outputPaths.insert(realisation.outPath); + builtOutputs.emplace(outputName, realisation); + } + + runPostBuildHook( + worker.store, + *logger, + drvPath, + outputPaths + ); + } + + auto status = resolvedResult.status; + if (status == BuildResult::AlreadyValid) + status = BuildResult::ResolvesToAlreadyValid; + + co_return done(status, std::move(builtOutputs)); + } + + /* If we get this far, we know no dynamic drvs inputs */ + + for (auto & [depDrvPath, depNode] : fullDrv.inputDrvs.map) { + for (auto & outputName : depNode.value) { + /* Don't need to worry about `inputGoals`, because + impure derivations are always resolved above. Can + just use DB. This case only happens in the (older) + input addressed and fixed output derivation cases. */ + auto outMap = [&]{ + for (auto * drvStore : { &worker.evalStore, &worker.store }) + if (drvStore->isValidPath(depDrvPath)) + return worker.store.queryDerivationOutputMap(depDrvPath, drvStore); + assert(false); + }(); + + auto outMapPath = outMap.find(outputName); + if (outMapPath == outMap.end()) { + throw Error( + "derivation '%s' requires non-existent output '%s' from input derivation '%s'", + worker.store.printStorePath(drvPath), outputName, worker.store.printStorePath(depDrvPath)); + } + + worker.store.computeFSClosure(outMapPath->second, inputPaths); + } + } + } + + /* Second, the input sources. */ + worker.store.computeFSClosure(drv->inputSrcs, inputPaths); + + debug("added input paths %s", worker.store.showPaths(inputPaths)); + + /* Okay, try to build. Note that here we don't wait for a build + slot to become available, since we don't need one if there is a + build hook. */ + co_await yield(); + co_return tryToBuild(); +} + +void DerivationBuildingGoal::started() +{ + auto msg = fmt( + buildMode == bmRepair ? "repairing outputs of '%s'" : + buildMode == bmCheck ? "checking outputs of '%s'" : + "building '%s'", worker.store.printStorePath(drvPath)); + fmt("building '%s'", worker.store.printStorePath(drvPath)); +#ifndef _WIN32 // TODO enable build hook on Windows + if (hook) msg += fmt(" on '%s'", machineName); +#endif + act = std::make_unique(*logger, lvlInfo, actBuild, msg, + Logger::Fields{worker.store.printStorePath(drvPath), +#ifndef _WIN32 // TODO enable build hook on Windows + hook ? machineName : +#endif + "", + 1, + 1}); + mcRunningBuilds = std::make_unique>(worker.runningBuilds); + worker.updateProgress(); +} + +Goal::Co DerivationBuildingGoal::tryToBuild() +{ + trace("trying to build"); + + /* Obtain locks on all output paths, if the paths are known a priori. + + The locks are automatically released when we exit this function or Nix + crashes. If we can't acquire the lock, then continue; hopefully some + other goal can start a build, and if not, the main loop will sleep a few + seconds and then retry this goal. */ + PathSet lockFiles; + /* FIXME: Should lock something like the drv itself so we don't build same + CA drv concurrently */ + if (dynamic_cast(&worker.store)) { + /* If we aren't a local store, we might need to use the local store as + a build remote, but that would cause a deadlock. */ + /* FIXME: Make it so we can use ourselves as a build remote even if we + are the local store (separate locking for building vs scheduling? */ + /* FIXME: find some way to lock for scheduling for the other stores so + a forking daemon with --store still won't farm out redundant builds. + */ + for (auto & i : drv->outputsAndOptPaths(worker.store)) { + if (i.second.second) + lockFiles.insert(worker.store.Store::toRealPath(*i.second.second)); + else + lockFiles.insert( + worker.store.Store::toRealPath(drvPath) + "." + i.first + ); + } + } + + if (!outputLocks.lockPaths(lockFiles, "", false)) + { + Activity act(*logger, lvlWarn, actBuildWaiting, + fmt("waiting for lock on %s", Magenta(showPaths(lockFiles)))); + + /* Wait then try locking again, repeat until success (returned + boolean is true). */ + do { + co_await waitForAWhile(); + } while (!outputLocks.lockPaths(lockFiles, "", false)); + } + + /* Now check again whether the outputs are valid. This is because + another process may have started building in parallel. After + it has finished and released the locks, we can (and should) + reuse its results. (Strictly speaking the first check can be + omitted, but that would be less efficient.) Note that since we + now hold the locks on the output paths, no other process can + build this derivation, so no further checks are necessary. */ + auto [allValid, validOutputs] = checkPathValidity(); + + if (buildMode != bmCheck && allValid) { + debug("skipping build of derivation '%s', someone beat us to it", worker.store.printStorePath(drvPath)); + outputLocks.setDeletion(true); + outputLocks.unlock(); + co_return done(BuildResult::AlreadyValid, std::move(validOutputs)); + } + + /* If any of the outputs already exist but are not valid, delete + them. */ + for (auto & [_, status] : initialOutputs) { + if (!status.known || status.known->isValid()) continue; + auto storePath = status.known->path; + debug("removing invalid path '%s'", worker.store.printStorePath(status.known->path)); + deletePath(worker.store.Store::toRealPath(storePath)); + } + + /* Don't do a remote build if the derivation has the attribute + `preferLocalBuild' set. Also, check and repair modes are only + supported for local builds. */ + bool buildLocally = + (buildMode != bmNormal || drvOptions->willBuildLocally(worker.store, *drv)) + && settings.maxBuildJobs.get() != 0; + + if (!buildLocally) { + switch (tryBuildHook()) { + case rpAccept: + /* Yes, it has started doing so. Wait until we get + EOF from the hook. */ + actLock.reset(); + buildResult.startTime = time(0); // inexact + started(); + co_await Suspend{}; + co_return hookDone(); + case rpPostpone: + /* Not now; wait until at least one child finishes or + the wake-up timeout expires. */ + if (!actLock) + actLock = std::make_unique(*logger, lvlWarn, actBuildWaiting, + fmt("waiting for a machine to build '%s'", Magenta(worker.store.printStorePath(drvPath)))); + outputLocks.unlock(); + co_await waitForAWhile(); + co_return tryToBuild(); + case rpDecline: + /* We should do it ourselves. */ + break; + } + } + + actLock.reset(); + + co_await yield(); + + if (!dynamic_cast(&worker.store)) { + throw Error( + R"( + Unable to build with a primary store that isn't a local store; + either pass a different '--store' or enable remote builds. + + For more information check 'man nix.conf' and search for '/machines'. + )" + ); + } + +#ifdef _WIN32 // TODO enable `DerivationBuilder` on Windows + throw UnimplementedError("building derivations is not yet implemented on Windows"); +#else + + // Will continue here while waiting for a build user below + while (true) { + + assert(!hook); + + unsigned int curBuilds = worker.getNrLocalBuilds(); + if (curBuilds >= settings.maxBuildJobs) { + outputLocks.unlock(); + co_await waitForBuildSlot(); + co_return tryToBuild(); + } + + if (!builder) { + /** + * Local implementation of these virtual methods, consider + * this just a record of lambdas. + */ + struct DerivationBuildingGoalCallbacks : DerivationBuilderCallbacks + { + DerivationBuildingGoal & goal; + + DerivationBuildingGoalCallbacks(DerivationBuildingGoal & goal, std::unique_ptr & builder) + : goal{goal} + {} + + ~DerivationBuildingGoalCallbacks() override = default; + + void childStarted(Descriptor builderOut) override + { + goal.worker.childStarted(goal.shared_from_this(), {builderOut}, true, true); + } + + void childTerminated() override + { + goal.worker.childTerminated(&goal); + } + + void noteHashMismatch() override + { + goal.worker.hashMismatch = true; + } + + void noteCheckMismatch() override + { + goal.worker.checkMismatch = true; + } + + void markContentsGood(const StorePath & path) override + { + goal.worker.markContentsGood(path); + } + + Path openLogFile() override { + return goal.openLogFile(); + } + void closeLogFile() override { + goal.closeLogFile(); + } + void appendLogTailErrorMsg(std::string & msg) override { + goal.appendLogTailErrorMsg(msg); + } + }; + + /* If we have to wait and retry (see below), then `builder` will + already be created, so we don't need to create it again. */ + builder = makeDerivationBuilder( + worker.store, + std::make_unique(*this, builder), + DerivationBuilderParams { + drvPath, + buildMode, + buildResult, + *drv, + parsedDrv.get(), + *drvOptions, + inputPaths, + initialOutputs, + }); + } + + if (!builder->prepareBuild()) { + if (!actLock) + actLock = std::make_unique(*logger, lvlWarn, actBuildWaiting, + fmt("waiting for a free build user ID for '%s'", Magenta(worker.store.printStorePath(drvPath)))); + co_await waitForAWhile(); + continue; + } + + break; + } + + actLock.reset(); + + try { + + /* Okay, we have to build. */ + builder->startBuilder(); + + } catch (BuildError & e) { + builder.reset(); + outputLocks.unlock(); + worker.permanentFailure = true; + co_return done(BuildResult::InputRejected, {}, std::move(e)); + } + + started(); + co_await Suspend{}; + + trace("build done"); + + auto res = builder->unprepareBuild(); + // N.B. cannot use `std::visit` with co-routine return + if (auto * ste = std::get_if<0>(&res)) { + outputLocks.unlock(); + co_return done(std::move(ste->first), {}, std::move(ste->second)); + } else if (auto * builtOutputs = std::get_if<1>(&res)) { + /* It is now safe to delete the lock files, since all future + lockers will see that the output paths are valid; they will + not create new lock files with the same names as the old + (unlinked) lock files. */ + outputLocks.setDeletion(true); + outputLocks.unlock(); + co_return done(BuildResult::Built, std::move(*builtOutputs)); + } else { + unreachable(); + } +#endif +} + + +void runPostBuildHook( + Store & store, + Logger & logger, + const StorePath & drvPath, + const StorePathSet & outputPaths) +{ + auto hook = settings.postBuildHook; + if (hook == "") + return; + + Activity act(logger, lvlTalkative, actPostBuildHook, + fmt("running post-build-hook '%s'", settings.postBuildHook), + Logger::Fields{store.printStorePath(drvPath)}); + PushActivity pact(act.id); + StringMap hookEnvironment = getEnv(); + + hookEnvironment.emplace("DRV_PATH", store.printStorePath(drvPath)); + hookEnvironment.emplace("OUT_PATHS", chomp(concatStringsSep(" ", store.printStorePathSet(outputPaths)))); + hookEnvironment.emplace("NIX_CONFIG", globalConfig.toKeyValue()); + + struct LogSink : Sink { + Activity & act; + std::string currentLine; + + LogSink(Activity & act) : act(act) { } + + void operator() (std::string_view data) override { + for (auto c : data) { + if (c == '\n') { + flushLine(); + } else { + currentLine += c; + } + } + } + + void flushLine() { + act.result(resPostBuildLogLine, currentLine); + currentLine.clear(); + } + + ~LogSink() { + if (currentLine != "") { + currentLine += '\n'; + flushLine(); + } + } + }; + LogSink sink(act); + + runProgram2({ + .program = settings.postBuildHook, + .environment = hookEnvironment, + .standardOut = &sink, + .mergeStderrToStdout = true, + }); +} + + +void DerivationBuildingGoal::appendLogTailErrorMsg(std::string & msg) +{ + if (!logger->isVerbose() && !logTail.empty()) { + msg += fmt("\nLast %d log lines:\n", logTail.size()); + for (auto & line : logTail) { + msg += "> "; + msg += line; + msg += "\n"; + } + auto nixLogCommand = experimentalFeatureSettings.isEnabled(Xp::NixCommand) + ? "nix log" + : "nix-store -l"; + // The command is on a separate line for easy copying, such as with triple click. + // This message will be indented elsewhere, so removing the indentation before the + // command will not put it at the start of the line unfortunately. + msg += fmt("For full logs, run:\n " ANSI_BOLD "%s %s" ANSI_NORMAL, + nixLogCommand, + worker.store.printStorePath(drvPath)); + } +} + + +Goal::Co DerivationBuildingGoal::hookDone() +{ +#ifndef _WIN32 + assert(hook); +#endif + + trace("hook build done"); + + /* Since we got an EOF on the logger pipe, the builder is presumed + to have terminated. In fact, the builder could also have + simply have closed its end of the pipe, so just to be sure, + kill it. */ + int status = +#ifndef _WIN32 // TODO enable build hook on Windows + hook->pid.kill(); +#else + 0; +#endif + + debug("build hook for '%s' finished", worker.store.printStorePath(drvPath)); + + buildResult.timesBuilt++; + buildResult.stopTime = time(0); + + /* So the child is gone now. */ + worker.childTerminated(this); + + /* Close the read side of the logger pipe. */ +#ifndef _WIN32 // TODO enable build hook on Windows + hook->builderOut.readSide.close(); + hook->fromHook.readSide.close(); +#endif + + /* Close the log file. */ + closeLogFile(); + + /* Check the exit status. */ + if (!statusOk(status)) { + auto msg = fmt( + "Cannot build '%s'.\n" + "Reason: " ANSI_RED "builder %s" ANSI_NORMAL ".", + Magenta(worker.store.printStorePath(drvPath)), + statusToString(status)); + + msg += showKnownOutputs(worker.store, *drv); + + appendLogTailErrorMsg(msg); + + outputLocks.unlock(); + + /* TODO (once again) support fine-grained error codes, see issue #12641. */ + + co_return done(BuildResult::MiscFailure, {}, BuildError(msg)); + } + + /* Compute the FS closure of the outputs and register them as + being valid. */ + auto builtOutputs = + /* When using a build hook, the build hook can register the output + as valid (by doing `nix-store --import'). If so we don't have + to do anything here. + + We can only early return when the outputs are known a priori. For + floating content-addressing derivations this isn't the case. + */ + assertPathValidity(); + + StorePathSet outputPaths; + for (auto & [_, output] : builtOutputs) + outputPaths.insert(output.outPath); + runPostBuildHook( + worker.store, + *logger, + drvPath, + outputPaths + ); + + /* It is now safe to delete the lock files, since all future + lockers will see that the output paths are valid; they will + not create new lock files with the same names as the old + (unlinked) lock files. */ + outputLocks.setDeletion(true); + outputLocks.unlock(); + + co_return done(BuildResult::Built, std::move(builtOutputs)); +} + +HookReply DerivationBuildingGoal::tryBuildHook() +{ +#ifdef _WIN32 // TODO enable build hook on Windows + return rpDecline; +#else + /* This should use `worker.evalStore`, but per #13179 the build hook + doesn't work with eval store anyways. */ + if (settings.buildHook.get().empty() || !worker.tryBuildHook || !worker.store.isValidPath(drvPath)) return rpDecline; + + if (!worker.hook) + worker.hook = std::make_unique(); + + try { + + /* Send the request to the hook. */ + worker.hook->sink + << "try" + << (worker.getNrLocalBuilds() < settings.maxBuildJobs ? 1 : 0) + << drv->platform + << worker.store.printStorePath(drvPath) + << drvOptions->getRequiredSystemFeatures(*drv); + worker.hook->sink.flush(); + + /* Read the first line of input, which should be a word indicating + whether the hook wishes to perform the build. */ + std::string reply; + while (true) { + auto s = [&]() { + try { + return readLine(worker.hook->fromHook.readSide.get()); + } catch (Error & e) { + e.addTrace({}, "while reading the response from the build hook"); + throw; + } + }(); + if (handleJSONLogMessage(s, worker.act, worker.hook->activities, "the build hook", true)) + ; + else if (s.substr(0, 2) == "# ") { + reply = s.substr(2); + break; + } + else { + s += "\n"; + writeToStderr(s); + } + } + + debug("hook reply is '%1%'", reply); + + if (reply == "decline") + return rpDecline; + else if (reply == "decline-permanently") { + worker.tryBuildHook = false; + worker.hook = 0; + return rpDecline; + } + else if (reply == "postpone") + return rpPostpone; + else if (reply != "accept") + throw Error("bad hook reply '%s'", reply); + + } catch (SysError & e) { + if (e.errNo == EPIPE) { + printError( + "build hook died unexpectedly: %s", + chomp(drainFD(worker.hook->fromHook.readSide.get()))); + worker.hook = 0; + return rpDecline; + } else + throw; + } + + hook = std::move(worker.hook); + + try { + machineName = readLine(hook->fromHook.readSide.get()); + } catch (Error & e) { + e.addTrace({}, "while reading the machine name from the build hook"); + throw; + } + + CommonProto::WriteConn conn { hook->sink }; + + /* Tell the hook all the inputs that have to be copied to the + remote system. */ + CommonProto::write(worker.store, conn, inputPaths); + + /* Tell the hooks the missing outputs that have to be copied back + from the remote system. */ + { + StringSet missingOutputs; + for (auto & [outputName, status] : initialOutputs) { + // XXX: Does this include known CA outputs? + if (buildMode != bmCheck && status.known && status.known->isValid()) continue; + missingOutputs.insert(outputName); + } + CommonProto::write(worker.store, conn, missingOutputs); + } + + hook->sink = FdSink(); + hook->toHook.writeSide.close(); + + /* Create the log file and pipe. */ + [[maybe_unused]] Path logFile = openLogFile(); + + std::set fds; + fds.insert(hook->fromHook.readSide.get()); + fds.insert(hook->builderOut.readSide.get()); + worker.childStarted(shared_from_this(), fds, false, false); + + return rpAccept; +#endif +} + + +Path DerivationBuildingGoal::openLogFile() +{ + logSize = 0; + + if (!settings.keepLog) return ""; + + auto baseName = std::string(baseNameOf(worker.store.printStorePath(drvPath))); + + /* Create a log file. */ + Path logDir; + if (auto localStore = dynamic_cast(&worker.store)) + logDir = localStore->config->logDir; + else + logDir = settings.nixLogDir; + Path dir = fmt("%s/%s/%s/", logDir, LocalFSStore::drvsLogDir, baseName.substr(0, 2)); + createDirs(dir); + + Path logFileName = fmt("%s/%s%s", dir, baseName.substr(2), + settings.compressLog ? ".bz2" : ""); + + fdLogFile = toDescriptor(open(logFileName.c_str(), O_CREAT | O_WRONLY | O_TRUNC +#ifndef _WIN32 + | O_CLOEXEC +#endif + , 0666)); + if (!fdLogFile) throw SysError("creating log file '%1%'", logFileName); + + logFileSink = std::make_shared(fdLogFile.get()); + + if (settings.compressLog) + logSink = std::shared_ptr(makeCompressionSink("bzip2", *logFileSink)); + else + logSink = logFileSink; + + return logFileName; +} + + +void DerivationBuildingGoal::closeLogFile() +{ + auto logSink2 = std::dynamic_pointer_cast(logSink); + if (logSink2) logSink2->finish(); + if (logFileSink) logFileSink->flush(); + logSink = logFileSink = 0; + fdLogFile.close(); +} + + +bool DerivationBuildingGoal::isReadDesc(Descriptor fd) +{ +#ifdef _WIN32 // TODO enable build hook on Windows + return false; +#else + return + (hook && fd == hook->builderOut.readSide.get()) + || + (builder && fd == builder->builderOut.get()); +#endif +} + +void DerivationBuildingGoal::handleChildOutput(Descriptor fd, std::string_view data) +{ + // local & `ssh://`-builds are dealt with here. + auto isWrittenToLog = isReadDesc(fd); + if (isWrittenToLog) + { + logSize += data.size(); + if (settings.maxLogSize && logSize > settings.maxLogSize) { + killChild(); + // We're not inside a coroutine, hence we can't use co_return here. + // Thus we ignore the return value. + [[maybe_unused]] Done _ = done( + BuildResult::LogLimitExceeded, {}, + Error("%s killed after writing more than %d bytes of log output", + getName(), settings.maxLogSize)); + return; + } + + for (auto c : data) + if (c == '\r') + currentLogLinePos = 0; + else if (c == '\n') + flushLine(); + else { + if (currentLogLinePos >= currentLogLine.size()) + currentLogLine.resize(currentLogLinePos + 1); + currentLogLine[currentLogLinePos++] = c; + } + + if (logSink) (*logSink)(data); + } + +#ifndef _WIN32 // TODO enable build hook on Windows + if (hook && fd == hook->fromHook.readSide.get()) { + for (auto c : data) + if (c == '\n') { + auto json = parseJSONMessage(currentHookLine, "the derivation builder"); + if (json) { + auto s = handleJSONLogMessage(*json, worker.act, hook->activities, "the derivation builder", true); + // ensure that logs from a builder using `ssh-ng://` as protocol + // are also available to `nix log`. + if (s && !isWrittenToLog && logSink) { + const auto type = (*json)["type"]; + const auto fields = (*json)["fields"]; + if (type == resBuildLogLine) { + (*logSink)((fields.size() > 0 ? fields[0].get() : "") + "\n"); + } else if (type == resSetPhase && ! fields.is_null()) { + const auto phase = fields[0]; + if (! phase.is_null()) { + // nixpkgs' stdenv produces lines in the log to signal + // phase changes. + // We want to get the same lines in case of remote builds. + // The format is: + // @nix { "action": "setPhase", "phase": "$curPhase" } + const auto logLine = nlohmann::json::object({ + {"action", "setPhase"}, + {"phase", phase} + }); + (*logSink)("@nix " + logLine.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace) + "\n"); + } + } + } + } + currentHookLine.clear(); + } else + currentHookLine += c; + } +#endif +} + + +void DerivationBuildingGoal::handleEOF(Descriptor fd) +{ + if (!currentLogLine.empty()) flushLine(); + worker.wakeUp(shared_from_this()); +} + + +void DerivationBuildingGoal::flushLine() +{ + if (handleJSONLogMessage(currentLogLine, *act, builderActivities, "the derivation builder", false)) + ; + + else { + logTail.push_back(currentLogLine); + if (logTail.size() > settings.logLines) logTail.pop_front(); + + act->result(resBuildLogLine, currentLogLine); + } + + currentLogLine = ""; + currentLogLinePos = 0; +} + + +std::map> DerivationBuildingGoal::queryPartialDerivationOutputMap() +{ + assert(!drv->type().isImpure()); + + for (auto * drvStore : { &worker.evalStore, &worker.store }) + if (drvStore->isValidPath(drvPath)) + return worker.store.queryPartialDerivationOutputMap(drvPath, drvStore); + + /* In-memory derivation will naturally fall back on this case, where + we do best-effort with static information. */ + std::map> res; + for (auto & [name, output] : drv->outputs) + res.insert_or_assign(name, output.path(worker.store, drv->name, name)); + return res; +} + +std::pair DerivationBuildingGoal::checkPathValidity() +{ + if (drv->type().isImpure()) return { false, {} }; + + bool checkHash = buildMode == bmRepair; + SingleDrvOutputs validOutputs; + + for (auto & i : queryPartialDerivationOutputMap()) { + auto initialOutput = get(initialOutputs, i.first); + if (!initialOutput) + // this is an invalid output, gets caught with (!wantedOutputsLeft.empty()) + continue; + auto & info = *initialOutput; + info.wanted = true; + if (i.second) { + auto outputPath = *i.second; + info.known = { + .path = outputPath, + .status = !worker.store.isValidPath(outputPath) + ? PathStatus::Absent + : !checkHash || worker.pathContentsGood(outputPath) + ? PathStatus::Valid + : PathStatus::Corrupt, + }; + } + auto drvOutput = DrvOutput{info.outputHash, i.first}; + if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations)) { + if (auto real = worker.store.queryRealisation(drvOutput)) { + info.known = { + .path = real->outPath, + .status = PathStatus::Valid, + }; + } else if (info.known && info.known->isValid()) { + // We know the output because it's a static output of the + // derivation, and the output path is valid, but we don't have + // its realisation stored (probably because it has been built + // without the `ca-derivations` experimental flag). + worker.store.registerDrvOutput( + Realisation { + drvOutput, + info.known->path, + } + ); + } + } + if (info.known && info.known->isValid()) + validOutputs.emplace(i.first, Realisation { drvOutput, info.known->path }); + } + + bool allValid = true; + for (auto & [_, status] : initialOutputs) { + if (!status.wanted) continue; + if (!status.known || !status.known->isValid()) { + allValid = false; + break; + } + } + + return { allValid, validOutputs }; +} + + +SingleDrvOutputs DerivationBuildingGoal::assertPathValidity() +{ + auto [allValid, validOutputs] = checkPathValidity(); + if (!allValid) + throw Error("some outputs are unexpectedly invalid"); + return validOutputs; +} + + +Goal::Done DerivationBuildingGoal::done( + BuildResult::Status status, + SingleDrvOutputs builtOutputs, + std::optional ex) +{ + outputLocks.unlock(); + buildResult.status = status; + if (ex) + buildResult.errorMsg = fmt("%s", Uncolored(ex->info().msg)); + if (buildResult.status == BuildResult::TimedOut) + worker.timedOut = true; + if (buildResult.status == BuildResult::PermanentFailure) + worker.permanentFailure = true; + + mcRunningBuilds.reset(); + + if (buildResult.success()) { + buildResult.builtOutputs = std::move(builtOutputs); + if (status == BuildResult::Built) + worker.doneBuilds++; + } else { + if (status != BuildResult::DependencyFailed) + worker.failedBuilds++; + } + + worker.updateProgress(); + + auto traceBuiltOutputsFile = getEnv("_NIX_TRACE_BUILT_OUTPUTS").value_or(""); + if (traceBuiltOutputsFile != "") { + std::fstream fs; + fs.open(traceBuiltOutputsFile, std::fstream::out); + fs << worker.store.printStorePath(drvPath) << "\t" << buildResult.toString() << std::endl; + } + + return amDone(buildResult.success() ? ecSuccess : ecFailed, std::move(ex)); +} + +} diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index bf1a25db1f0..128d360c967 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -1,73 +1,34 @@ -#include "derivation-goal.hh" +#include "nix/store/build/derivation-goal.hh" +#include "nix/store/build/derivation-building-goal.hh" #ifndef _WIN32 // TODO enable build hook on Windows -# include "hook-instance.hh" +# include "nix/store/build/hook-instance.hh" +# include "nix/store/build/derivation-builder.hh" #endif -#include "processes.hh" -#include "config-global.hh" -#include "worker.hh" -#include "builtins.hh" -#include "builtins/buildenv.hh" -#include "references.hh" -#include "finally.hh" -#include "util.hh" -#include "archive.hh" -#include "compression.hh" -#include "common-protocol.hh" -#include "common-protocol-impl.hh" -#include "topo-sort.hh" -#include "callback.hh" -#include "local-store.hh" // TODO remove, along with remaining downcasts - -#include -#include +#include "nix/util/processes.hh" +#include "nix/util/config-global.hh" +#include "nix/store/build/worker.hh" +#include "nix/util/util.hh" +#include "nix/util/compression.hh" +#include "nix/store/common-protocol.hh" +#include "nix/store/common-protocol-impl.hh" // Don't remove is actually needed +#include "nix/store/local-store.hh" // TODO remove, along with remaining downcasts #include #include #include #include -#ifndef _WIN32 // TODO abstract over proc exit status -# include -#endif - #include -#include "strings.hh" +#include "nix/util/strings.hh" namespace nix { -Goal::Co DerivationGoal::init() { - if (useDerivation) { - co_return getDerivation(); - } else { - co_return haveDerivation(); - } -} - -DerivationGoal::DerivationGoal(const StorePath & drvPath, - const OutputsSpec & wantedOutputs, Worker & worker, BuildMode buildMode) - : Goal(worker, DerivedPath::Built { .drvPath = makeConstantStorePathRef(drvPath), .outputs = wantedOutputs }) - , useDerivation(true) - , drvPath(drvPath) - , wantedOutputs(wantedOutputs) - , buildMode(buildMode) -{ - name = fmt( - "building of '%s' from .drv file", - DerivedPath::Built { makeConstantStorePathRef(drvPath), wantedOutputs }.to_string(worker.store)); - trace("created"); - - mcExpectedBuilds = std::make_unique>(worker.expectedBuilds); - worker.updateProgress(); -} - - -DerivationGoal::DerivationGoal(const StorePath & drvPath, const BasicDerivation & drv, - const OutputsSpec & wantedOutputs, Worker & worker, BuildMode buildMode) - : Goal(worker, DerivedPath::Built { .drvPath = makeConstantStorePathRef(drvPath), .outputs = wantedOutputs }) - , useDerivation(false) +DerivationGoal::DerivationGoal(const StorePath & drvPath, const Derivation & drv, + const OutputName & wantedOutput, Worker & worker, BuildMode buildMode) + : Goal(worker, haveDerivation()) , drvPath(drvPath) - , wantedOutputs(wantedOutputs) + , wantedOutput(wantedOutput) , buildMode(buildMode) { this->drv = std::make_unique(drv); @@ -80,17 +41,6 @@ DerivationGoal::DerivationGoal(const StorePath & drvPath, const BasicDerivation mcExpectedBuilds = std::make_unique>(worker.expectedBuilds); worker.updateProgress(); - /* Prevent the .chroot directory from being - garbage-collected. (See isActiveTempFile() in gc.cc.) */ - worker.store.addTempRoot(this->drvPath); -} - - -DerivationGoal::~DerivationGoal() -{ - /* Careful: we should never ever throw an exception from a - destructor. */ - try { closeLogFile(); } catch (...) { ignoreExceptionInDestructor(); } } @@ -100,157 +50,132 @@ std::string DerivationGoal::key() i.e. a derivation named "aardvark" always comes before "baboon". And substitution goals always happen before derivation goals (due to "b$"). */ - return "b$" + std::string(drvPath.name()) + "$" + worker.store.printStorePath(drvPath); -} - - -void DerivationGoal::killChild() -{ -#ifndef _WIN32 // TODO enable build hook on Windows - hook.reset(); -#endif -} - - -void DerivationGoal::timedOut(Error && ex) -{ - killChild(); - // We're not inside a coroutine, hence we can't use co_return here. - // Thus we ignore the return value. - [[maybe_unused]] Done _ = done(BuildResult::TimedOut, {}, std::move(ex)); -} - -void DerivationGoal::addWantedOutputs(const OutputsSpec & outputs) -{ - auto newWanted = wantedOutputs.union_(outputs); - switch (needRestart) { - case NeedRestartForMoreOutputs::OutputsUnmodifedDontNeed: - if (!newWanted.isSubsetOf(wantedOutputs)) - needRestart = NeedRestartForMoreOutputs::OutputsAddedDoNeed; - break; - case NeedRestartForMoreOutputs::OutputsAddedDoNeed: - /* No need to check whether we added more outputs, because a - restart is already queued up. */ - break; - case NeedRestartForMoreOutputs::BuildInProgressWillNotNeed: - /* We are already building all outputs, so it doesn't matter if - we now want more. */ - break; - }; - wantedOutputs = newWanted; + return "b$" + std::string(drvPath.name()) + "$" + SingleDerivedPath::Built{ + .drvPath = makeConstantStorePathRef(drvPath), + .output = wantedOutput, + }.to_string(worker.store); } -Goal::Co DerivationGoal::getDerivation() +Goal::Co DerivationGoal::haveDerivation() { - trace("init"); + trace("have derivation"); - /* The first thing to do is to make sure that the derivation - exists. If it doesn't, it may be created through a - substitute. */ - if (buildMode == bmNormal && worker.evalStore.isValidPath(drvPath)) { - co_return loadDerivation(); - } + auto drvOptions = [&]() -> DerivationOptions { + auto parsedOpt = StructuredAttrs::tryParse(drv->env); + try { + return DerivationOptions::fromStructuredAttrs(drv->env, parsedOpt ? &*parsedOpt : nullptr); + } catch (Error & e) { + e.addTrace({}, "while parsing derivation '%s'", worker.store.printStorePath(drvPath)); + throw; + } + }(); - addWaitee(upcast_goal(worker.makePathSubstitutionGoal(drvPath))); + if (!drv->type().hasKnownOutputPaths()) + experimentalFeatureSettings.require(Xp::CaDerivations); - co_await Suspend{}; - co_return loadDerivation(); -} + /* At least one of the output paths could not be + produced using a substitute. So we have to build instead. */ + auto gaveUpOnSubstitution = [&]() -> Goal::Co + { + auto g = worker.makeDerivationBuildingGoal(drvPath, *drv, buildMode); + /* We will finish with it ourselves, as if we were the derivational goal. */ + g->preserveException = true; -Goal::Co DerivationGoal::loadDerivation() -{ - trace("loading derivation"); + // TODO move into constructor + g->initialOutputs = initialOutputs; - if (nrFailed != 0) { - co_return done(BuildResult::MiscFailure, {}, Error("cannot build missing derivation '%s'", worker.store.printStorePath(drvPath))); - } + { + Goals waitees; + waitees.insert(g); + co_await await(std::move(waitees)); + } - /* `drvPath' should already be a root, but let's be on the safe - side: if the user forgot to make it a root, we wouldn't want - things being garbage collected while we're busy. */ - worker.evalStore.addTempRoot(drvPath); + trace("outer build done"); - /* Get the derivation. It is probably in the eval store, but it might be inthe main store: + buildResult = g->buildResult; - - Resolved derivation are resolved against main store realisations, and so must be stored there. + if (buildMode == bmCheck) { + /* In checking mode, the builder will not register any outputs. + So we want to make sure the ones that we wanted to check are + properly there. */ + buildResult.builtOutputs = assertPathValidity(); + } - - Dynamic derivations are built, and so are found in the main store. - */ - for (auto * drvStore : { &worker.evalStore, &worker.store }) { - if (drvStore->isValidPath(drvPath)) { - drv = std::make_unique(drvStore->readDerivation(drvPath)); - break; + for (auto it = buildResult.builtOutputs.begin(); it != buildResult.builtOutputs.end(); ) { + if (it->first != wantedOutput) { + it = buildResult.builtOutputs.erase(it); + } else { + ++it; + } } - } - assert(drv); - co_return haveDerivation(); -} + if (buildResult.success()) + assert(buildResult.builtOutputs.count(wantedOutput) > 0); + co_return amDone(g->exitCode, g->ex); + }; -Goal::Co DerivationGoal::haveDerivation() -{ - trace("have derivation"); + for (auto & i : drv->outputsAndOptPaths(worker.store)) + if (i.second.second) + worker.store.addTempRoot(*i.second.second); - parsedDrv = std::make_unique(drvPath, *drv); + { + bool impure = drv->type().isImpure(); - if (!drv->type().hasKnownOutputPaths()) - experimentalFeatureSettings.require(Xp::CaDerivations); + if (impure) experimentalFeatureSettings.require(Xp::ImpureDerivations); + + auto outputHashes = staticOutputHashes(worker.evalStore, *drv); + for (auto & [outputName, outputHash] : outputHashes) { + InitialOutput v{ + .wanted = true, // Will be refined later + .outputHash = outputHash + }; - if (drv->type().isImpure()) { - experimentalFeatureSettings.require(Xp::ImpureDerivations); + /* TODO we might want to also allow randomizing the paths + for regular CA derivations, e.g. for sake of checking + determinism. */ + if (impure) { + v.known = InitialOutputStatus { + .path = StorePath::random(outputPathName(drv->name, outputName)), + .status = PathStatus::Absent, + }; + } - for (auto & [outputName, output] : drv->outputs) { - auto randomPath = StorePath::random(outputPathName(drv->name, outputName)); - assert(!worker.store.isValidPath(randomPath)); initialOutputs.insert({ outputName, - InitialOutput { - .wanted = true, - .outputHash = impureOutputHash, - .known = InitialOutputStatus { - .path = randomPath, - .status = PathStatus::Absent - } - } + std::move(v), }); } - co_return gaveUpOnSubstitution(); + if (impure) { + /* We don't yet have any safe way to cache an impure derivation at + this step. */ + co_return gaveUpOnSubstitution(); + } } - for (auto & i : drv->outputsAndOptPaths(worker.store)) - if (i.second.second) - worker.store.addTempRoot(*i.second.second); - - auto outputHashes = staticOutputHashes(worker.evalStore, *drv); - for (auto & [outputName, outputHash] : outputHashes) - initialOutputs.insert({ - outputName, - InitialOutput { - .wanted = true, // Will be refined later - .outputHash = outputHash - } - }); - - /* Check what outputs paths are not already valid. */ - auto [allValid, validOutputs] = checkPathValidity(); + { + /* Check what outputs paths are not already valid. */ + auto [allValid, validOutputs] = checkPathValidity(); - /* If they are all valid, then we're done. */ - if (allValid && buildMode == bmNormal) { - co_return done(BuildResult::AlreadyValid, std::move(validOutputs)); + /* If they are all valid, then we're done. */ + if (allValid && buildMode == bmNormal) { + co_return done(BuildResult::AlreadyValid, std::move(validOutputs)); + } } + Goals waitees; + /* We are first going to try to create the invalid output paths through substitutes. If that doesn't work, we'll build them. */ - if (settings.useSubstitutes && parsedDrv->substitutesAllowed()) + if (settings.useSubstitutes && drvOptions.substitutesAllowed()) for (auto & [outputName, status] : initialOutputs) { if (!status.wanted) continue; if (!status.known) - addWaitee( + waitees.insert( upcast_goal( worker.makeDrvOutputSubstitutionGoal( DrvOutput{status.outputHash, outputName}, @@ -260,63 +185,26 @@ Goal::Co DerivationGoal::haveDerivation() ); else { auto * cap = getDerivationCA(*drv); - addWaitee(upcast_goal(worker.makePathSubstitutionGoal( + waitees.insert(upcast_goal(worker.makePathSubstitutionGoal( status.known->path, buildMode == bmRepair ? Repair : NoRepair, cap ? std::optional { *cap } : std::nullopt))); } } - if (!waitees.empty()) co_await Suspend{}; /* to prevent hang (no wake-up event) */ - co_return outputsSubstitutionTried(); -} - + co_await await(std::move(waitees)); -Goal::Co DerivationGoal::outputsSubstitutionTried() -{ trace("all outputs substituted (maybe)"); assert(!drv->type().isImpure()); - if (nrFailed > 0 && nrFailed > nrNoSubstituters + nrIncompleteClosure && !settings.tryFallback) { + if (nrFailed > 0 && nrFailed > nrNoSubstituters && !settings.tryFallback) { co_return done(BuildResult::TransientFailure, {}, Error("some substitutes for the outputs of derivation '%s' failed (usually happens due to networking issues); try '--fallback' to build derivation from source ", worker.store.printStorePath(drvPath))); } - /* If the substitutes form an incomplete closure, then we should - build the dependencies of this derivation, but after that, we - can still use the substitutes for this derivation itself. - - If the nrIncompleteClosure != nrFailed, we have another issue as well. - In particular, it may be the case that the hole in the closure is - an output of the current derivation, which causes a loop if retried. - */ - { - bool substitutionFailed = - nrIncompleteClosure > 0 && - nrIncompleteClosure == nrFailed; - switch (retrySubstitution) { - case RetrySubstitution::NoNeed: - if (substitutionFailed) - retrySubstitution = RetrySubstitution::YesNeed; - break; - case RetrySubstitution::YesNeed: - // Should not be able to reach this state from here. - assert(false); - break; - case RetrySubstitution::AlreadyRetried: - debug("substitution failed again, but we already retried once. Not retrying again."); - break; - } - } - - nrFailed = nrNoSubstituters = nrIncompleteClosure = 0; - - if (needRestart == NeedRestartForMoreOutputs::OutputsAddedDoNeed) { - needRestart = NeedRestartForMoreOutputs::OutputsUnmodifedDontNeed; - co_return haveDerivation(); - } + nrFailed = nrNoSubstituters = 0; auto [allValid, validOutputs] = checkPathValidity(); @@ -335,72 +223,16 @@ Goal::Co DerivationGoal::outputsSubstitutionTried() } -/* At least one of the output paths could not be - produced using a substitute. So we have to build instead. */ -Goal::Co DerivationGoal::gaveUpOnSubstitution() +/** + * Used for `inputGoals` local variable below + */ +struct value_comparison { - /* At this point we are building all outputs, so if more are wanted there - is no need to restart. */ - needRestart = NeedRestartForMoreOutputs::BuildInProgressWillNotNeed; - - /* The inputs must be built before we can build this goal. */ - inputDrvOutputs.clear(); - if (useDerivation) { - std::function, const DerivedPathMap::ChildNode &)> addWaiteeDerivedPath; - - addWaiteeDerivedPath = [&](ref inputDrv, const DerivedPathMap::ChildNode & inputNode) { - if (!inputNode.value.empty()) - addWaitee(worker.makeGoal( - DerivedPath::Built { - .drvPath = inputDrv, - .outputs = inputNode.value, - }, - buildMode == bmRepair ? bmRepair : bmNormal)); - for (const auto & [outputName, childNode] : inputNode.childMap) - addWaiteeDerivedPath( - make_ref(SingleDerivedPath::Built { inputDrv, outputName }), - childNode); - }; - - for (const auto & [inputDrvPath, inputNode] : dynamic_cast(drv.get())->inputDrvs.map) { - /* Ensure that pure, non-fixed-output derivations don't - depend on impure derivations. */ - if (experimentalFeatureSettings.isEnabled(Xp::ImpureDerivations) && !drv->type().isImpure() && !drv->type().isFixed()) { - auto inputDrv = worker.evalStore.readDerivation(inputDrvPath); - if (inputDrv.type().isImpure()) - throw Error("pure derivation '%s' depends on impure derivation '%s'", - worker.store.printStorePath(drvPath), - worker.store.printStorePath(inputDrvPath)); - } - - addWaiteeDerivedPath(makeConstantStorePathRef(inputDrvPath), inputNode); - } - } - - /* Copy the input sources from the eval store to the build - store. - - Note that some inputs might not be in the eval store because they - are (resolved) derivation outputs in a resolved derivation. */ - if (&worker.evalStore != &worker.store) { - RealisedPath::Set inputSrcs; - for (auto & i : drv->inputSrcs) - if (worker.evalStore.isValidPath(i)) - inputSrcs.insert(i); - copyClosure(worker.evalStore, worker.store, inputSrcs); - } - - for (auto & i : drv->inputSrcs) { - if (worker.store.isValidPath(i)) continue; - if (!settings.useSubstitutes) - throw Error("dependency '%s' of '%s' does not exist, and substitution is disabled", - worker.store.printStorePath(i), worker.store.printStorePath(drvPath)); - addWaitee(upcast_goal(worker.makePathSubstitutionGoal(i))); + template + bool operator()(const ref & lhs, const ref & rhs) const { + return *lhs < *rhs; } - - if (!waitees.empty()) co_await Suspend{}; /* to prevent hang (no wake-up event) */ - co_return inputsRealised(); -} +}; Goal::Co DerivationGoal::repairClosure() @@ -415,9 +247,8 @@ Goal::Co DerivationGoal::repairClosure() /* Get the output closure. */ auto outputs = queryDerivationOutputMap(); StorePathSet outputClosure; - for (auto & i : outputs) { - if (!wantedOutputs.contains(i.first)) continue; - worker.store.computeFSClosure(i.second, outputClosure); + if (auto mPath = get(outputs, wantedOutput)) { + worker.store.computeFSClosure(*mPath, outputClosure); } /* Filter out our own outputs (which we have already checked). */ @@ -428,7 +259,12 @@ Goal::Co DerivationGoal::repairClosure() derivation is responsible for which path in the output closure. */ StorePathSet inputClosure; - if (useDerivation) worker.store.computeFSClosure(drvPath, inputClosure); + + /* If we're working from an in-memory derivation with no in-store + `*.drv` file, we cannot do this part. */ + if (worker.store.isValidPath(drvPath)) + worker.store.computeFSClosure(drvPath, inputClosure); + std::map outputsToDrv; for (auto & i : inputClosure) if (i.isDerivation()) { @@ -438,6 +274,8 @@ Goal::Co DerivationGoal::repairClosure() outputsToDrv.insert_or_assign(*j.second, i); } + Goals waitees; + /* Check each path (slow!). */ for (auto & i : outputClosure) { if (worker.pathContentsGood(i)) continue; @@ -446,9 +284,9 @@ Goal::Co DerivationGoal::repairClosure() worker.store.printStorePath(i), worker.store.printStorePath(drvPath)); auto drvPath2 = outputsToDrv.find(i); if (drvPath2 == outputsToDrv.end()) - addWaitee(upcast_goal(worker.makePathSubstitutionGoal(i, Repair))); + waitees.insert(upcast_goal(worker.makePathSubstitutionGoal(i, Repair))); else - addWaitee(worker.makeGoal( + waitees.insert(worker.makeGoal( DerivedPath::Built { .drvPath = makeConstantStorePathRef(drvPath2->second), .outputs = OutputsSpec::All { }, @@ -456,985 +294,47 @@ Goal::Co DerivationGoal::repairClosure() bmRepair)); } - if (waitees.empty()) { - co_return done(BuildResult::AlreadyValid, assertPathValidity()); - } else { - co_await Suspend{}; - co_return closureRepaired(); - } -} - - -Goal::Co DerivationGoal::closureRepaired() -{ - trace("closure repaired"); - if (nrFailed > 0) - throw Error("some paths in the output closure of derivation '%s' could not be repaired", - worker.store.printStorePath(drvPath)); - co_return done(BuildResult::AlreadyValid, assertPathValidity()); -} - - -Goal::Co DerivationGoal::inputsRealised() -{ - trace("all inputs realised"); - - if (nrFailed != 0) { - if (!useDerivation) - throw Error("some dependencies of '%s' are missing", worker.store.printStorePath(drvPath)); - co_return done(BuildResult::DependencyFailed, {}, Error( - "%s dependencies of derivation '%s' failed to build", - nrFailed, worker.store.printStorePath(drvPath))); - } - - if (retrySubstitution == RetrySubstitution::YesNeed) { - retrySubstitution = RetrySubstitution::AlreadyRetried; - co_return haveDerivation(); - } - - /* Gather information necessary for computing the closure and/or - running the build hook. */ - - /* Determine the full set of input paths. */ - - /* First, the input derivations. */ - if (useDerivation) { - auto & fullDrv = *dynamic_cast(drv.get()); - - auto drvType = fullDrv.type(); - bool resolveDrv = std::visit(overloaded { - [&](const DerivationType::InputAddressed & ia) { - /* must resolve if deferred. */ - return ia.deferred; - }, - [&](const DerivationType::ContentAddressed & ca) { - return !fullDrv.inputDrvs.map.empty() && ( - ca.fixed - /* Can optionally resolve if fixed, which is good - for avoiding unnecessary rebuilds. */ - ? experimentalFeatureSettings.isEnabled(Xp::CaDerivations) - /* Must resolve if floating and there are any inputs - drvs. */ - : true); - }, - [&](const DerivationType::Impure &) { - return true; - } - }, drvType.raw); - - if (resolveDrv && !fullDrv.inputDrvs.map.empty()) { - experimentalFeatureSettings.require(Xp::CaDerivations); - - /* We are be able to resolve this derivation based on the - now-known results of dependencies. If so, we become a - stub goal aliasing that resolved derivation goal. */ - std::optional attempt = fullDrv.tryResolve(worker.store, inputDrvOutputs); - if (!attempt) { - /* TODO (impure derivations-induced tech debt) (see below): - The above attempt should have found it, but because we manage - inputDrvOutputs statefully, sometimes it gets out of sync with - the real source of truth (store). So we query the store - directly if there's a problem. */ - attempt = fullDrv.tryResolve(worker.store, &worker.evalStore); - } - assert(attempt); - Derivation drvResolved { std::move(*attempt) }; - - auto pathResolved = writeDerivation(worker.store, drvResolved); - - auto msg = fmt("resolved derivation: '%s' -> '%s'", - worker.store.printStorePath(drvPath), - worker.store.printStorePath(pathResolved)); - act = std::make_unique(*logger, lvlInfo, actBuildWaiting, msg, - Logger::Fields { - worker.store.printStorePath(drvPath), - worker.store.printStorePath(pathResolved), - }); - - resolvedDrvGoal = worker.makeDerivationGoal( - pathResolved, wantedOutputs, buildMode); - addWaitee(resolvedDrvGoal); - - co_await Suspend{}; - co_return resolvedFinished(); - } - - std::function::ChildNode &)> accumInputPaths; - - accumInputPaths = [&](const StorePath & depDrvPath, const DerivedPathMap::ChildNode & inputNode) { - /* Add the relevant output closures of the input derivation - `i' as input paths. Only add the closures of output paths - that are specified as inputs. */ - auto getOutput = [&](const std::string & outputName) { - /* TODO (impure derivations-induced tech debt): - Tracking input derivation outputs statefully through the - goals is error prone and has led to bugs. - For a robust nix, we need to move towards the `else` branch, - which does not rely on goal state to match up with the - reality of the store, which is our real source of truth. - However, the impure derivations feature still relies on this - fragile way of doing things, because its builds do not have - a representation in the store, which is a usability problem - in itself. When implementing this logic entirely with lookups - make sure that they're cached. */ - if (auto outPath = get(inputDrvOutputs, { depDrvPath, outputName })) { - return *outPath; - } - else { - auto outMap = [&]{ - for (auto * drvStore : { &worker.evalStore, &worker.store }) - if (drvStore->isValidPath(depDrvPath)) - return worker.store.queryDerivationOutputMap(depDrvPath, drvStore); - assert(false); - }(); - - auto outMapPath = outMap.find(outputName); - if (outMapPath == outMap.end()) { - throw Error( - "derivation '%s' requires non-existent output '%s' from input derivation '%s'", - worker.store.printStorePath(drvPath), outputName, worker.store.printStorePath(depDrvPath)); - } - return outMapPath->second; - } - }; - - for (auto & outputName : inputNode.value) - worker.store.computeFSClosure(getOutput(outputName), inputPaths); - - for (auto & [outputName, childNode] : inputNode.childMap) - accumInputPaths(getOutput(outputName), childNode); - }; - - for (auto & [depDrvPath, depNode] : fullDrv.inputDrvs.map) - accumInputPaths(depDrvPath, depNode); - } - - /* Second, the input sources. */ - worker.store.computeFSClosure(drv->inputSrcs, inputPaths); - - debug("added input paths %s", worker.store.showPaths(inputPaths)); - - /* What type of derivation are we building? */ - derivationType = drv->type(); - - /* Okay, try to build. Note that here we don't wait for a build - slot to become available, since we don't need one if there is a - build hook. */ - worker.wakeUp(shared_from_this()); - co_await Suspend{}; - co_return tryToBuild(); -} - -void DerivationGoal::started() -{ - auto msg = fmt( - buildMode == bmRepair ? "repairing outputs of '%s'" : - buildMode == bmCheck ? "checking outputs of '%s'" : - "building '%s'", worker.store.printStorePath(drvPath)); - fmt("building '%s'", worker.store.printStorePath(drvPath)); -#ifndef _WIN32 // TODO enable build hook on Windows - if (hook) msg += fmt(" on '%s'", machineName); -#endif - act = std::make_unique(*logger, lvlInfo, actBuild, msg, - Logger::Fields{worker.store.printStorePath(drvPath), -#ifndef _WIN32 // TODO enable build hook on Windows - hook ? machineName : -#endif - "", - 1, - 1}); - mcRunningBuilds = std::make_unique>(worker.runningBuilds); - worker.updateProgress(); -} - -Goal::Co DerivationGoal::tryToBuild() -{ - trace("trying to build"); - - /* Obtain locks on all output paths, if the paths are known a priori. - - The locks are automatically released when we exit this function or Nix - crashes. If we can't acquire the lock, then continue; hopefully some - other goal can start a build, and if not, the main loop will sleep a few - seconds and then retry this goal. */ - PathSet lockFiles; - /* FIXME: Should lock something like the drv itself so we don't build same - CA drv concurrently */ - if (dynamic_cast(&worker.store)) { - /* If we aren't a local store, we might need to use the local store as - a build remote, but that would cause a deadlock. */ - /* FIXME: Make it so we can use ourselves as a build remote even if we - are the local store (separate locking for building vs scheduling? */ - /* FIXME: find some way to lock for scheduling for the other stores so - a forking daemon with --store still won't farm out redundant builds. - */ - for (auto & i : drv->outputsAndOptPaths(worker.store)) { - if (i.second.second) - lockFiles.insert(worker.store.Store::toRealPath(*i.second.second)); - else - lockFiles.insert( - worker.store.Store::toRealPath(drvPath) + "." + i.first - ); - } - } - - if (!outputLocks.lockPaths(lockFiles, "", false)) { - if (!actLock) - actLock = std::make_unique(*logger, lvlWarn, actBuildWaiting, - fmt("waiting for lock on %s", Magenta(showPaths(lockFiles)))); - worker.waitForAWhile(shared_from_this()); - co_await Suspend{}; - co_return tryToBuild(); - } - - actLock.reset(); - - /* Now check again whether the outputs are valid. This is because - another process may have started building in parallel. After - it has finished and released the locks, we can (and should) - reuse its results. (Strictly speaking the first check can be - omitted, but that would be less efficient.) Note that since we - now hold the locks on the output paths, no other process can - build this derivation, so no further checks are necessary. */ - auto [allValid, validOutputs] = checkPathValidity(); - - if (buildMode != bmCheck && allValid) { - debug("skipping build of derivation '%s', someone beat us to it", worker.store.printStorePath(drvPath)); - outputLocks.setDeletion(true); - co_return done(BuildResult::AlreadyValid, std::move(validOutputs)); - } - - /* If any of the outputs already exist but are not valid, delete - them. */ - for (auto & [_, status] : initialOutputs) { - if (!status.known || status.known->isValid()) continue; - auto storePath = status.known->path; - debug("removing invalid path '%s'", worker.store.printStorePath(status.known->path)); - deletePath(worker.store.Store::toRealPath(storePath)); - } - - /* Don't do a remote build if the derivation has the attribute - `preferLocalBuild' set. Also, check and repair modes are only - supported for local builds. */ - bool buildLocally = - (buildMode != bmNormal || parsedDrv->willBuildLocally(worker.store)) - && settings.maxBuildJobs.get() != 0; - - if (!buildLocally) { - switch (tryBuildHook()) { - case rpAccept: - /* Yes, it has started doing so. Wait until we get - EOF from the hook. */ - actLock.reset(); - buildResult.startTime = time(0); // inexact - started(); - co_await Suspend{}; - co_return buildDone(); - case rpPostpone: - /* Not now; wait until at least one child finishes or - the wake-up timeout expires. */ - if (!actLock) - actLock = std::make_unique(*logger, lvlWarn, actBuildWaiting, - fmt("waiting for a machine to build '%s'", Magenta(worker.store.printStorePath(drvPath)))); - worker.waitForAWhile(shared_from_this()); - outputLocks.unlock(); - co_await Suspend{}; - co_return tryToBuild(); - case rpDecline: - /* We should do it ourselves. */ - break; - } - } - - actLock.reset(); - - worker.wakeUp(shared_from_this()); - co_await Suspend{}; - co_return tryLocalBuild(); -} - -Goal::Co DerivationGoal::tryLocalBuild() { - throw Error( - R"( - Unable to build with a primary store that isn't a local store; - either pass a different '--store' or enable remote builds. - - For more information check 'man nix.conf' and search for '/machines'. - )" - ); -} - - -static void chmod_(const Path & path, mode_t mode) -{ - if (chmod(path.c_str(), mode) == -1) - throw SysError("setting permissions on '%s'", path); -} - - -/* Move/rename path 'src' to 'dst'. Temporarily make 'src' writable if - it's a directory and we're not root (to be able to update the - directory's parent link ".."). */ -static void movePath(const Path & src, const Path & dst) -{ - auto st = lstat(src); - - bool changePerm = ( -#ifndef _WIN32 - geteuid() -#else - !isRootUser() -#endif - && S_ISDIR(st.st_mode) && !(st.st_mode & S_IWUSR)); - - if (changePerm) - chmod_(src, st.st_mode | S_IWUSR); - - std::filesystem::rename(src, dst); - - if (changePerm) - chmod_(dst, st.st_mode); -} - - -void replaceValidPath(const Path & storePath, const Path & tmpPath) -{ - /* We can't atomically replace storePath (the original) with - tmpPath (the replacement), so we have to move it out of the - way first. We'd better not be interrupted here, because if - we're repairing (say) Glibc, we end up with a broken system. */ - Path oldPath = fmt("%1%.old-%2%-%3%", storePath, getpid(), rand()); - if (pathExists(storePath)) - movePath(storePath, oldPath); - - try { - movePath(tmpPath, storePath); - } catch (...) { - try { - // attempt to recover - movePath(oldPath, storePath); - } catch (...) { - ignoreExceptionExceptInterrupt(); - } - throw; - } - - deletePath(oldPath); -} - - -int DerivationGoal::getChildStatus() -{ -#ifndef _WIN32 // TODO enable build hook on Windows - return hook->pid.kill(); -#else - return 0; -#endif -} - - -void DerivationGoal::closeReadPipes() -{ -#ifndef _WIN32 // TODO enable build hook on Windows - hook->builderOut.readSide.close(); - hook->fromHook.readSide.close(); -#endif -} - - -void DerivationGoal::cleanupHookFinally() -{ -} - - -void DerivationGoal::cleanupPreChildKill() -{ -} - - -void DerivationGoal::cleanupPostChildKill() -{ -} - - -bool DerivationGoal::cleanupDecideWhetherDiskFull() -{ - return false; -} - - -void DerivationGoal::cleanupPostOutputsRegisteredModeCheck() -{ -} - - -void DerivationGoal::cleanupPostOutputsRegisteredModeNonCheck() -{ -} - -void runPostBuildHook( - Store & store, - Logger & logger, - const StorePath & drvPath, - const StorePathSet & outputPaths) -{ - auto hook = settings.postBuildHook; - if (hook == "") - return; - - Activity act(logger, lvlTalkative, actPostBuildHook, - fmt("running post-build-hook '%s'", settings.postBuildHook), - Logger::Fields{store.printStorePath(drvPath)}); - PushActivity pact(act.id); - std::map hookEnvironment = getEnv(); - - hookEnvironment.emplace("DRV_PATH", store.printStorePath(drvPath)); - hookEnvironment.emplace("OUT_PATHS", chomp(concatStringsSep(" ", store.printStorePathSet(outputPaths)))); - hookEnvironment.emplace("NIX_CONFIG", globalConfig.toKeyValue()); - - struct LogSink : Sink { - Activity & act; - std::string currentLine; - - LogSink(Activity & act) : act(act) { } - - void operator() (std::string_view data) override { - for (auto c : data) { - if (c == '\n') { - flushLine(); - } else { - currentLine += c; - } - } - } + co_await await(std::move(waitees)); - void flushLine() { - act.result(resPostBuildLogLine, currentLine); - currentLine.clear(); - } - - ~LogSink() { - if (currentLine != "") { - currentLine += '\n'; - flushLine(); - } - } - }; - LogSink sink(act); - - runProgram2({ - .program = settings.postBuildHook, - .environment = hookEnvironment, - .standardOut = &sink, - .mergeStderrToStdout = true, - }); -} - -Goal::Co DerivationGoal::buildDone() -{ - trace("build done"); - - Finally releaseBuildUser([&](){ this->cleanupHookFinally(); }); - - cleanupPreChildKill(); - - /* Since we got an EOF on the logger pipe, the builder is presumed - to have terminated. In fact, the builder could also have - simply have closed its end of the pipe, so just to be sure, - kill it. */ - int status = getChildStatus(); - - debug("builder process for '%s' finished", worker.store.printStorePath(drvPath)); - - buildResult.timesBuilt++; - buildResult.stopTime = time(0); - - /* So the child is gone now. */ - worker.childTerminated(this); - - /* Close the read side of the logger pipe. */ - closeReadPipes(); - - /* Close the log file. */ - closeLogFile(); - - cleanupPostChildKill(); - - if (buildResult.cpuUser && buildResult.cpuSystem) { - debug("builder for '%s' terminated with status %d, user CPU %.3fs, system CPU %.3fs", - worker.store.printStorePath(drvPath), - status, - ((double) buildResult.cpuUser->count()) / 1000000, - ((double) buildResult.cpuSystem->count()) / 1000000); + if (!waitees.empty()) { + trace("closure repaired"); + if (nrFailed > 0) + throw Error("some paths in the output closure of derivation '%s' could not be repaired", + worker.store.printStorePath(drvPath)); } - - bool diskFull = false; - - try { - - /* Check the exit status. */ - if (!statusOk(status)) { - - diskFull |= cleanupDecideWhetherDiskFull(); - - auto msg = fmt("builder for '%s' %s", - Magenta(worker.store.printStorePath(drvPath)), - statusToString(status)); - - if (!logger->isVerbose() && !logTail.empty()) { - msg += fmt(";\nlast %d log lines:\n", logTail.size()); - for (auto & line : logTail) { - msg += "> "; - msg += line; - msg += "\n"; - } - auto nixLogCommand = experimentalFeatureSettings.isEnabled(Xp::NixCommand) - ? "nix log" - : "nix-store -l"; - // The command is on a separate line for easy copying, such as with triple click. - // This message will be indented elsewhere, so removing the indentation before the - // command will not put it at the start of the line unfortunately. - msg += fmt("For full logs, run:\n " ANSI_BOLD "%s %s" ANSI_NORMAL, - nixLogCommand, - worker.store.printStorePath(drvPath)); - } - - if (diskFull) - msg += "\nnote: build failure may have been caused by lack of free disk space"; - - throw BuildError(msg); - } - - /* Compute the FS closure of the outputs and register them as - being valid. */ - auto builtOutputs = registerOutputs(); - - StorePathSet outputPaths; - for (auto & [_, output] : builtOutputs) - outputPaths.insert(output.outPath); - runPostBuildHook( - worker.store, - *logger, - drvPath, - outputPaths - ); - - cleanupPostOutputsRegisteredModeNonCheck(); - - /* It is now safe to delete the lock files, since all future - lockers will see that the output paths are valid; they will - not create new lock files with the same names as the old - (unlinked) lock files. */ - outputLocks.setDeletion(true); - outputLocks.unlock(); - - co_return done(BuildResult::Built, std::move(builtOutputs)); - - } catch (BuildError & e) { - outputLocks.unlock(); - - BuildResult::Status st = BuildResult::MiscFailure; - -#ifndef _WIN32 // TODO abstract over proc exit status - if (hook && WIFEXITED(status) && WEXITSTATUS(status) == 101) - st = BuildResult::TimedOut; - - else if (hook && (!WIFEXITED(status) || WEXITSTATUS(status) != 100)) { - } - - else -#endif - { - assert(derivationType); - st = - dynamic_cast(&e) ? BuildResult::NotDeterministic : - statusOk(status) ? BuildResult::OutputRejected : - !derivationType->isSandboxed() || diskFull ? BuildResult::TransientFailure : - BuildResult::PermanentFailure; - } - - co_return done(st, {}, std::move(e)); - } -} - -Goal::Co DerivationGoal::resolvedFinished() -{ - trace("resolved derivation finished"); - - assert(resolvedDrvGoal); - auto resolvedDrv = *resolvedDrvGoal->drv; - auto & resolvedResult = resolvedDrvGoal->buildResult; - - SingleDrvOutputs builtOutputs; - - if (resolvedResult.success()) { - auto resolvedHashes = staticOutputHashes(worker.store, resolvedDrv); - - StorePathSet outputPaths; - - for (auto & outputName : resolvedDrv.outputNames()) { - auto initialOutput = get(initialOutputs, outputName); - auto resolvedHash = get(resolvedHashes, outputName); - if ((!initialOutput) || (!resolvedHash)) - throw Error( - "derivation '%s' doesn't have expected output '%s' (derivation-goal.cc/resolvedFinished,resolve)", - worker.store.printStorePath(drvPath), outputName); - - auto realisation = [&]{ - auto take1 = get(resolvedResult.builtOutputs, outputName); - if (take1) return *take1; - - /* The above `get` should work. But sateful tracking of - outputs in resolvedResult, this can get out of sync with the - store, which is our actual source of truth. For now we just - check the store directly if it fails. */ - auto take2 = worker.evalStore.queryRealisation(DrvOutput { *resolvedHash, outputName }); - if (take2) return *take2; - - throw Error( - "derivation '%s' doesn't have expected output '%s' (derivation-goal.cc/resolvedFinished,realisation)", - worker.store.printStorePath(resolvedDrvGoal->drvPath), outputName); - }(); - - if (!drv->type().isImpure()) { - auto newRealisation = realisation; - newRealisation.id = DrvOutput { initialOutput->outputHash, outputName }; - newRealisation.signatures.clear(); - if (!drv->type().isFixed()) { - auto & drvStore = worker.evalStore.isValidPath(drvPath) - ? worker.evalStore - : worker.store; - newRealisation.dependentRealisations = drvOutputReferences(worker.store, *drv, realisation.outPath, &drvStore); - } - signRealisation(newRealisation); - worker.store.registerDrvOutput(newRealisation); - } - outputPaths.insert(realisation.outPath); - builtOutputs.emplace(outputName, realisation); - } - - runPostBuildHook( - worker.store, - *logger, - drvPath, - outputPaths - ); - } - - auto status = resolvedResult.status; - if (status == BuildResult::AlreadyValid) - status = BuildResult::ResolvesToAlreadyValid; - - co_return done(status, std::move(builtOutputs)); -} - -HookReply DerivationGoal::tryBuildHook() -{ -#ifdef _WIN32 // TODO enable build hook on Windows - return rpDecline; -#else - if (settings.buildHook.get().empty() || !worker.tryBuildHook || !useDerivation) return rpDecline; - - if (!worker.hook) - worker.hook = std::make_unique(); - - try { - - /* Send the request to the hook. */ - worker.hook->sink - << "try" - << (worker.getNrLocalBuilds() < settings.maxBuildJobs ? 1 : 0) - << drv->platform - << worker.store.printStorePath(drvPath) - << parsedDrv->getRequiredSystemFeatures(); - worker.hook->sink.flush(); - - /* Read the first line of input, which should be a word indicating - whether the hook wishes to perform the build. */ - std::string reply; - while (true) { - auto s = [&]() { - try { - return readLine(worker.hook->fromHook.readSide.get()); - } catch (Error & e) { - e.addTrace({}, "while reading the response from the build hook"); - throw; - } - }(); - if (handleJSONLogMessage(s, worker.act, worker.hook->activities, true)) - ; - else if (s.substr(0, 2) == "# ") { - reply = s.substr(2); - break; - } - else { - s += "\n"; - writeToStderr(s); - } - } - - debug("hook reply is '%1%'", reply); - - if (reply == "decline") - return rpDecline; - else if (reply == "decline-permanently") { - worker.tryBuildHook = false; - worker.hook = 0; - return rpDecline; - } - else if (reply == "postpone") - return rpPostpone; - else if (reply != "accept") - throw Error("bad hook reply '%s'", reply); - - } catch (SysError & e) { - if (e.errNo == EPIPE) { - printError( - "build hook died unexpectedly: %s", - chomp(drainFD(worker.hook->fromHook.readSide.get()))); - worker.hook = 0; - return rpDecline; - } else - throw; - } - - hook = std::move(worker.hook); - - try { - machineName = readLine(hook->fromHook.readSide.get()); - } catch (Error & e) { - e.addTrace({}, "while reading the machine name from the build hook"); - throw; - } - - CommonProto::WriteConn conn { hook->sink }; - - /* Tell the hook all the inputs that have to be copied to the - remote system. */ - CommonProto::write(worker.store, conn, inputPaths); - - /* Tell the hooks the missing outputs that have to be copied back - from the remote system. */ - { - StringSet missingOutputs; - for (auto & [outputName, status] : initialOutputs) { - // XXX: Does this include known CA outputs? - if (buildMode != bmCheck && status.known && status.known->isValid()) continue; - missingOutputs.insert(outputName); - } - CommonProto::write(worker.store, conn, missingOutputs); - } - - hook->sink = FdSink(); - hook->toHook.writeSide.close(); - - /* Create the log file and pipe. */ - [[maybe_unused]] Path logFile = openLogFile(); - - std::set fds; - fds.insert(hook->fromHook.readSide.get()); - fds.insert(hook->builderOut.readSide.get()); - worker.childStarted(shared_from_this(), fds, false, false); - - return rpAccept; -#endif -} - - -SingleDrvOutputs DerivationGoal::registerOutputs() -{ - /* When using a build hook, the build hook can register the output - as valid (by doing `nix-store --import'). If so we don't have - to do anything here. - - We can only early return when the outputs are known a priori. For - floating content-addressed derivations this isn't the case. - */ - return assertPathValidity(); -} - -Path DerivationGoal::openLogFile() -{ - logSize = 0; - - if (!settings.keepLog) return ""; - - auto baseName = std::string(baseNameOf(worker.store.printStorePath(drvPath))); - - /* Create a log file. */ - Path logDir; - if (auto localStore = dynamic_cast(&worker.store)) - logDir = localStore->logDir; - else - logDir = settings.nixLogDir; - Path dir = fmt("%s/%s/%s/", logDir, LocalFSStore::drvsLogDir, baseName.substr(0, 2)); - createDirs(dir); - - Path logFileName = fmt("%s/%s%s", dir, baseName.substr(2), - settings.compressLog ? ".bz2" : ""); - - fdLogFile = toDescriptor(open(logFileName.c_str(), O_CREAT | O_WRONLY | O_TRUNC -#ifndef _WIN32 - | O_CLOEXEC -#endif - , 0666)); - if (!fdLogFile) throw SysError("creating log file '%1%'", logFileName); - - logFileSink = std::make_shared(fdLogFile.get()); - - if (settings.compressLog) - logSink = std::shared_ptr(makeCompressionSink("bzip2", *logFileSink)); - else - logSink = logFileSink; - - return logFileName; -} - - -void DerivationGoal::closeLogFile() -{ - auto logSink2 = std::dynamic_pointer_cast(logSink); - if (logSink2) logSink2->finish(); - if (logFileSink) logFileSink->flush(); - logSink = logFileSink = 0; - fdLogFile.close(); -} - - -bool DerivationGoal::isReadDesc(Descriptor fd) -{ -#ifdef _WIN32 // TODO enable build hook on Windows - return false; -#else - return fd == hook->builderOut.readSide.get(); -#endif -} - -void DerivationGoal::handleChildOutput(Descriptor fd, std::string_view data) -{ - // local & `ssh://`-builds are dealt with here. - auto isWrittenToLog = isReadDesc(fd); - if (isWrittenToLog) - { - logSize += data.size(); - if (settings.maxLogSize && logSize > settings.maxLogSize) { - killChild(); - // We're not inside a coroutine, hence we can't use co_return here. - // Thus we ignore the return value. - [[maybe_unused]] Done _ = done( - BuildResult::LogLimitExceeded, {}, - Error("%s killed after writing more than %d bytes of log output", - getName(), settings.maxLogSize)); - return; - } - - for (auto c : data) - if (c == '\r') - currentLogLinePos = 0; - else if (c == '\n') - flushLine(); - else { - if (currentLogLinePos >= currentLogLine.size()) - currentLogLine.resize(currentLogLinePos + 1); - currentLogLine[currentLogLinePos++] = c; - } - - if (logSink) (*logSink)(data); - } - -#ifndef _WIN32 // TODO enable build hook on Windows - if (hook && fd == hook->fromHook.readSide.get()) { - for (auto c : data) - if (c == '\n') { - auto json = parseJSONMessage(currentHookLine); - if (json) { - auto s = handleJSONLogMessage(*json, worker.act, hook->activities, true); - // ensure that logs from a builder using `ssh-ng://` as protocol - // are also available to `nix log`. - if (s && !isWrittenToLog && logSink) { - const auto type = (*json)["type"]; - const auto fields = (*json)["fields"]; - if (type == resBuildLogLine) { - (*logSink)((fields.size() > 0 ? fields[0].get() : "") + "\n"); - } else if (type == resSetPhase && ! fields.is_null()) { - const auto phase = fields[0]; - if (! phase.is_null()) { - // nixpkgs' stdenv produces lines in the log to signal - // phase changes. - // We want to get the same lines in case of remote builds. - // The format is: - // @nix { "action": "setPhase", "phase": "$curPhase" } - const auto logLine = nlohmann::json::object({ - {"action", "setPhase"}, - {"phase", phase} - }); - (*logSink)("@nix " + logLine.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace) + "\n"); - } - } - } - } - currentHookLine.clear(); - } else - currentHookLine += c; - } -#endif -} - - -void DerivationGoal::handleEOF(Descriptor fd) -{ - if (!currentLogLine.empty()) flushLine(); - worker.wakeUp(shared_from_this()); -} - - -void DerivationGoal::flushLine() -{ - if (handleJSONLogMessage(currentLogLine, *act, builderActivities, false)) - ; - - else { - logTail.push_back(currentLogLine); - if (logTail.size() > settings.logLines) logTail.pop_front(); - - act->result(resBuildLogLine, currentLogLine); - } - - currentLogLine = ""; - currentLogLinePos = 0; + co_return done(BuildResult::AlreadyValid, assertPathValidity()); } std::map> DerivationGoal::queryPartialDerivationOutputMap() { assert(!drv->type().isImpure()); - if (!useDerivation || drv->type().hasKnownOutputPaths()) { - std::map> res; - for (auto & [name, output] : drv->outputs) - res.insert_or_assign(name, output.path(worker.store, drv->name, name)); - return res; - } else { - for (auto * drvStore : { &worker.evalStore, &worker.store }) - if (drvStore->isValidPath(drvPath)) - return worker.store.queryPartialDerivationOutputMap(drvPath, drvStore); - assert(false); - } + + for (auto * drvStore : { &worker.evalStore, &worker.store }) + if (drvStore->isValidPath(drvPath)) + return worker.store.queryPartialDerivationOutputMap(drvPath, drvStore); + + /* In-memory derivation will naturally fall back on this case, where + we do best-effort with static information. */ + std::map> res; + for (auto & [name, output] : drv->outputs) + res.insert_or_assign(name, output.path(worker.store, drv->name, name)); + return res; } OutputPathMap DerivationGoal::queryDerivationOutputMap() { assert(!drv->type().isImpure()); - if (!useDerivation || drv->type().hasKnownOutputPaths()) { - OutputPathMap res; - for (auto & [name, output] : drv->outputsAndOptPaths(worker.store)) - res.insert_or_assign(name, *output.second); - return res; - } else { - for (auto * drvStore : { &worker.evalStore, &worker.store }) - if (drvStore->isValidPath(drvPath)) - return worker.store.queryDerivationOutputMap(drvPath, drvStore); - assert(false); - } + + for (auto * drvStore : { &worker.evalStore, &worker.store }) + if (drvStore->isValidPath(drvPath)) + return worker.store.queryDerivationOutputMap(drvPath, drvStore); + + // See comment in `DerivationGoal::queryPartialDerivationOutputMap`. + OutputPathMap res; + for (auto & [name, output] : drv->outputsAndOptPaths(worker.store)) + res.insert_or_assign(name, *output.second); + return res; } @@ -1443,23 +343,16 @@ std::pair DerivationGoal::checkPathValidity() if (drv->type().isImpure()) return { false, {} }; bool checkHash = buildMode == bmRepair; - auto wantedOutputsLeft = std::visit(overloaded { - [&](const OutputsSpec::All &) { - return StringSet {}; - }, - [&](const OutputsSpec::Names & names) { - return static_cast(names); - }, - }, wantedOutputs.raw); + StringSet wantedOutputsLeft{wantedOutput}; SingleDrvOutputs validOutputs; for (auto & i : queryPartialDerivationOutputMap()) { auto initialOutput = get(initialOutputs, i.first); if (!initialOutput) - // this is an invalid output, gets catched with (!wantedOutputsLeft.empty()) + // this is an invalid output, gets caught with (!wantedOutputsLeft.empty()) continue; auto & info = *initialOutput; - info.wanted = wantedOutputs.contains(i.first); + info.wanted = wantedOutput == i.first; if (info.wanted) wantedOutputsLeft.erase(i.first); if (i.second) { @@ -1532,7 +425,6 @@ Goal::Done DerivationGoal::done( SingleDrvOutputs builtOutputs, std::optional ex) { - outputLocks.unlock(); buildResult.status = status; if (ex) buildResult.errorMsg = fmt("%s", Uncolored(ex->info().msg)); @@ -1542,10 +434,9 @@ Goal::Done DerivationGoal::done( worker.permanentFailure = true; mcExpectedBuilds.reset(); - mcRunningBuilds.reset(); if (buildResult.success()) { - auto wantedBuiltOutputs = filterDrvOutputs(wantedOutputs, std::move(builtOutputs)); + auto wantedBuiltOutputs = filterDrvOutputs(OutputsSpec::Names{wantedOutput}, std::move(builtOutputs)); assert(!wantedBuiltOutputs.empty()); buildResult.builtOutputs = std::move(wantedBuiltOutputs); if (status == BuildResult::Built) @@ -1567,34 +458,4 @@ Goal::Done DerivationGoal::done( return amDone(buildResult.success() ? ecSuccess : ecFailed, std::move(ex)); } - -void DerivationGoal::waiteeDone(GoalPtr waitee, ExitCode result) -{ - Goal::waiteeDone(waitee, result); - - if (!useDerivation || !drv) return; - auto & fullDrv = *dynamic_cast(drv.get()); - - auto * dg = dynamic_cast(&*waitee); - if (!dg) return; - - auto * nodeP = fullDrv.inputDrvs.findSlot(DerivedPath::Opaque { .path = dg->drvPath }); - if (!nodeP) return; - auto & outputs = nodeP->value; - - for (auto & outputName : outputs) { - auto buildResult = dg->getBuildResult(DerivedPath::Built { - .drvPath = makeConstantStorePathRef(dg->drvPath), - .outputs = OutputsSpec::Names { outputName }, - }); - if (buildResult.success()) { - auto i = buildResult.builtOutputs.find(outputName); - if (i != buildResult.builtOutputs.end()) - inputDrvOutputs.insert_or_assign( - { dg->drvPath, outputName }, - i->second.outPath); - } - } -} - } diff --git a/src/libstore/build/derivation-goal.hh b/src/libstore/build/derivation-goal.hh deleted file mode 100644 index ad3d9ca2acf..00000000000 --- a/src/libstore/build/derivation-goal.hh +++ /dev/null @@ -1,348 +0,0 @@ -#pragma once -///@file - -#include "parsed-derivations.hh" -#ifndef _WIN32 -# include "user-lock.hh" -#endif -#include "outputs-spec.hh" -#include "store-api.hh" -#include "pathlocks.hh" -#include "goal.hh" - -namespace nix { - -using std::map; - -#ifndef _WIN32 // TODO enable build hook on Windows -struct HookInstance; -#endif - -typedef enum {rpAccept, rpDecline, rpPostpone} HookReply; - -/** - * Unless we are repairing, we don't both to test validity and just assume it, - * so the choices are `Absent` or `Valid`. - */ -enum struct PathStatus { - Corrupt, - Absent, - Valid, -}; - -struct InitialOutputStatus { - StorePath path; - PathStatus status; - /** - * Valid in the store, and additionally non-corrupt if we are repairing - */ - bool isValid() const { - return status == PathStatus::Valid; - } - /** - * Merely present, allowed to be corrupt - */ - bool isPresent() const { - return status == PathStatus::Corrupt - || status == PathStatus::Valid; - } -}; - -struct InitialOutput { - bool wanted; - Hash outputHash; - std::optional known; -}; - -/** - * A goal for building some or all of the outputs of a derivation. - */ -struct DerivationGoal : public Goal -{ - /** - * Whether to use an on-disk .drv file. - */ - bool useDerivation; - - /** The path of the derivation. */ - StorePath drvPath; - - /** - * The goal for the corresponding resolved derivation - */ - std::shared_ptr resolvedDrvGoal; - - /** - * The specific outputs that we need to build. - */ - OutputsSpec wantedOutputs; - - /** - * Mapping from input derivations + output names to actual store - * paths. This is filled in by waiteeDone() as each dependency - * finishes, before inputsRealised() is reached. - */ - std::map, StorePath> inputDrvOutputs; - - /** - * See `needRestart`; just for that field. - */ - enum struct NeedRestartForMoreOutputs { - /** - * The goal state machine is progressing based on the current value of - * `wantedOutputs. No actions are needed. - */ - OutputsUnmodifedDontNeed, - /** - * `wantedOutputs` has been extended, but the state machine is - * proceeding according to its old value, so we need to restart. - */ - OutputsAddedDoNeed, - /** - * The goal state machine has progressed to the point of doing a build, - * in which case all outputs will be produced, so extensions to - * `wantedOutputs` no longer require a restart. - */ - BuildInProgressWillNotNeed, - }; - - /** - * Whether additional wanted outputs have been added. - */ - NeedRestartForMoreOutputs needRestart = NeedRestartForMoreOutputs::OutputsUnmodifedDontNeed; - - /** - * See `retrySubstitution`; just for that field. - */ - enum RetrySubstitution { - /** - * No issues have yet arose, no need to restart. - */ - NoNeed, - /** - * Something failed and there is an incomplete closure. Let's retry - * substituting. - */ - YesNeed, - /** - * We are current or have already retried substitution, and whether or - * not something goes wrong we will not retry again. - */ - AlreadyRetried, - }; - - /** - * Whether to retry substituting the outputs after building the - * inputs. This is done in case of an incomplete closure. - */ - RetrySubstitution retrySubstitution = RetrySubstitution::NoNeed; - - /** - * The derivation stored at drvPath. - */ - std::unique_ptr drv; - - std::unique_ptr parsedDrv; - - /** - * The remainder is state held during the build. - */ - - /** - * Locks on (fixed) output paths. - */ - PathLocks outputLocks; - - /** - * All input paths (that is, the union of FS closures of the - * immediate input paths). - */ - StorePathSet inputPaths; - - std::map initialOutputs; - - /** - * File descriptor for the log file. - */ - AutoCloseFD fdLogFile; - std::shared_ptr logFileSink, logSink; - - /** - * Number of bytes received from the builder's stdout/stderr. - */ - unsigned long logSize; - - /** - * The most recent log lines. - */ - std::list logTail; - - std::string currentLogLine; - size_t currentLogLinePos = 0; // to handle carriage return - - std::string currentHookLine; - -#ifndef _WIN32 // TODO enable build hook on Windows - /** - * The build hook. - */ - std::unique_ptr hook; -#endif - - /** - * The sort of derivation we are building. - */ - std::optional derivationType; - - BuildMode buildMode; - - std::unique_ptr> mcExpectedBuilds, mcRunningBuilds; - - std::unique_ptr act; - - /** - * Activity that denotes waiting for a lock. - */ - std::unique_ptr actLock; - - std::map builderActivities; - - /** - * The remote machine on which we're building. - */ - std::string machineName; - - DerivationGoal(const StorePath & drvPath, - const OutputsSpec & wantedOutputs, Worker & worker, - BuildMode buildMode = bmNormal); - DerivationGoal(const StorePath & drvPath, const BasicDerivation & drv, - const OutputsSpec & wantedOutputs, Worker & worker, - BuildMode buildMode = bmNormal); - virtual ~DerivationGoal(); - - void timedOut(Error && ex) override; - - std::string key() override; - - /** - * Add wanted outputs to an already existing derivation goal. - */ - void addWantedOutputs(const OutputsSpec & outputs); - - /** - * The states. - */ - Co init() override; - Co getDerivation(); - Co loadDerivation(); - Co haveDerivation(); - Co outputsSubstitutionTried(); - Co gaveUpOnSubstitution(); - Co closureRepaired(); - Co inputsRealised(); - Co tryToBuild(); - virtual Co tryLocalBuild(); - Co buildDone(); - - Co resolvedFinished(); - - /** - * Is the build hook willing to perform the build? - */ - HookReply tryBuildHook(); - - virtual int getChildStatus(); - - /** - * Check that the derivation outputs all exist and register them - * as valid. - */ - virtual SingleDrvOutputs registerOutputs(); - - /** - * Open a log file and a pipe to it. - */ - Path openLogFile(); - - /** - * Sign the newly built realisation if the store allows it - */ - virtual void signRealisation(Realisation&) {} - - /** - * Close the log file. - */ - void closeLogFile(); - - /** - * Close the read side of the logger pipe. - */ - virtual void closeReadPipes(); - - /** - * Cleanup hooks for buildDone() - */ - virtual void cleanupHookFinally(); - virtual void cleanupPreChildKill(); - virtual void cleanupPostChildKill(); - virtual bool cleanupDecideWhetherDiskFull(); - virtual void cleanupPostOutputsRegisteredModeCheck(); - virtual void cleanupPostOutputsRegisteredModeNonCheck(); - - virtual bool isReadDesc(Descriptor fd); - - /** - * Callback used by the worker to write to the log. - */ - void handleChildOutput(Descriptor fd, std::string_view data) override; - void handleEOF(Descriptor fd) override; - void flushLine(); - - /** - * Wrappers around the corresponding Store methods that first consult the - * derivation. This is currently needed because when there is no drv file - * there also is no DB entry. - */ - std::map> queryPartialDerivationOutputMap(); - OutputPathMap queryDerivationOutputMap(); - - /** - * Update 'initialOutputs' to determine the current status of the - * outputs of the derivation. Also returns a Boolean denoting - * whether all outputs are valid and non-corrupt, and a - * 'SingleDrvOutputs' structure containing the valid outputs. - */ - std::pair checkPathValidity(); - - /** - * Aborts if any output is not valid or corrupt, and otherwise - * returns a 'SingleDrvOutputs' structure containing all outputs. - */ - SingleDrvOutputs assertPathValidity(); - - /** - * Forcibly kill the child process, if any. - */ - virtual void killChild(); - - Co repairClosure(); - - void started(); - - Done done( - BuildResult::Status status, - SingleDrvOutputs builtOutputs = {}, - std::optional ex = {}); - - void waiteeDone(GoalPtr waitee, ExitCode result) override; - - StorePathSet exportReferences(const StorePathSet & storePaths); - - JobCategory jobCategory() const override { - return JobCategory::Build; - }; -}; - -MakeError(NotDeterministic, BuildError); - -} diff --git a/src/libstore/build/derivation-trampoline-goal.cc b/src/libstore/build/derivation-trampoline-goal.cc new file mode 100644 index 00000000000..e8ca47dfe51 --- /dev/null +++ b/src/libstore/build/derivation-trampoline-goal.cc @@ -0,0 +1,175 @@ +#include "nix/store/build/derivation-trampoline-goal.hh" +#include "nix/store/build/worker.hh" +#include "nix/store/derivations.hh" + +namespace nix { + +DerivationTrampolineGoal::DerivationTrampolineGoal( + ref drvReq, const OutputsSpec & wantedOutputs, Worker & worker, BuildMode buildMode) + : Goal(worker, init()) + , drvReq(drvReq) + , wantedOutputs(wantedOutputs) + , buildMode(buildMode) +{ + commonInit(); +} + +DerivationTrampolineGoal::DerivationTrampolineGoal( + const StorePath & drvPath, + const OutputsSpec & wantedOutputs, + const Derivation & drv, + Worker & worker, + BuildMode buildMode) + : Goal(worker, haveDerivation(drvPath, drv)) + , drvReq(makeConstantStorePathRef(drvPath)) + , wantedOutputs(wantedOutputs) + , buildMode(buildMode) +{ + commonInit(); +} + +void DerivationTrampolineGoal::commonInit() +{ + name = + fmt("outer obtaining drv from '%s' and then building outputs %s", + drvReq->to_string(worker.store), + std::visit( + overloaded{ + [&](const OutputsSpec::All) -> std::string { return "* (all of them)"; }, + [&](const OutputsSpec::Names os) { return concatStringsSep(", ", quoteStrings(os)); }, + }, + wantedOutputs.raw)); + trace("created outer"); + + worker.updateProgress(); +} + +DerivationTrampolineGoal::~DerivationTrampolineGoal() {} + +static StorePath pathPartOfReq(const SingleDerivedPath & req) +{ + return std::visit( + overloaded{ + [&](const SingleDerivedPath::Opaque & bo) { return bo.path; }, + [&](const SingleDerivedPath::Built & bfd) { return pathPartOfReq(*bfd.drvPath); }, + }, + req.raw()); +} + +std::string DerivationTrampolineGoal::key() +{ + /* Ensure that derivations get built in order of their name, + i.e. a derivation named "aardvark" always comes before "baboon". And + substitution goals, derivation goals, and derivation building goals always happen before + derivation goals (due to "bt$"). */ + return "bt$" + std::string(pathPartOfReq(*drvReq).name()) + "$" + DerivedPath::Built{ + .drvPath = drvReq, + .outputs = wantedOutputs, + }.to_string(worker.store); +} + +void DerivationTrampolineGoal::timedOut(Error && ex) {} + +Goal::Co DerivationTrampolineGoal::init() +{ + trace("need to load derivation from file"); + + /* The first thing to do is to make sure that the derivation + exists. If it doesn't, it may be built from another derivation, + or merely substituted. We can make goal to get it and not worry + about which method it takes to get the derivation. */ + if (auto optDrvPath = [this]() -> std::optional { + if (buildMode != bmNormal) + return std::nullopt; + + auto drvPath = StorePath::dummy; + try { + drvPath = resolveDerivedPath(worker.store, *drvReq); + } catch (MissingRealisation &) { + return std::nullopt; + } + auto cond = worker.evalStore.isValidPath(drvPath) || worker.store.isValidPath(drvPath); + return cond ? std::optional{drvPath} : std::nullopt; + }()) { + trace( + fmt("already have drv '%s' for '%s', can go straight to building", + worker.store.printStorePath(*optDrvPath), + drvReq->to_string(worker.store))); + } else { + trace("need to obtain drv we want to build"); + Goals waitees{worker.makeGoal(DerivedPath::fromSingle(*drvReq))}; + co_await await(std::move(waitees)); + } + + trace("outer load and build derivation"); + + if (nrFailed != 0) { + co_return amDone(ecFailed, Error("cannot build missing derivation '%s'", drvReq->to_string(worker.store))); + } + + StorePath drvPath = resolveDerivedPath(worker.store, *drvReq); + + /* `drvPath' should already be a root, but let's be on the safe + side: if the user forgot to make it a root, we wouldn't want + things being garbage collected while we're busy. */ + worker.evalStore.addTempRoot(drvPath); + + /* Get the derivation. It is probably in the eval store, but it might be in the main store: + + - Resolved derivation are resolved against main store realisations, and so must be stored there. + + - Dynamic derivations are built, and so are found in the main store. + */ + auto drv = [&] { + for (auto * drvStore : {&worker.evalStore, &worker.store}) + if (drvStore->isValidPath(drvPath)) + return drvStore->readDerivation(drvPath); + assert(false); + }(); + + co_return haveDerivation(std::move(drvPath), std::move(drv)); +} + +Goal::Co DerivationTrampolineGoal::haveDerivation(StorePath drvPath, Derivation drv) +{ + trace("have derivation, will kick off derivations goals per wanted output"); + + auto resolvedWantedOutputs = std::visit( + overloaded{ + [&](const OutputsSpec::Names & names) -> OutputsSpec::Names { return names; }, + [&](const OutputsSpec::All &) -> OutputsSpec::Names { + StringSet outputs; + for (auto & [outputName, _] : drv.outputs) + outputs.insert(outputName); + return outputs; + }, + }, + wantedOutputs.raw); + + Goals concreteDrvGoals; + + /* Build this step! */ + + for (auto & output : resolvedWantedOutputs) { + auto g = upcast_goal(worker.makeDerivationGoal(drvPath, drv, output, buildMode)); + g->preserveException = true; + /* We will finish with it ourselves, as if we were the derivational goal. */ + concreteDrvGoals.insert(std::move(g)); + } + + // Copy on purpose + co_await await(Goals(concreteDrvGoals)); + + trace("outer build done"); + + auto & g = *concreteDrvGoals.begin(); + buildResult = g->buildResult; + for (auto & g2 : concreteDrvGoals) { + for (auto && [x, y] : g2->buildResult.builtOutputs) + buildResult.builtOutputs.insert_or_assign(x, y); + } + + co_return amDone(g->exitCode, g->ex); +} + +} diff --git a/src/libstore/build/drv-output-substitution-goal.cc b/src/libstore/build/drv-output-substitution-goal.cc index f069c0d9404..e87a796f6b5 100644 --- a/src/libstore/build/drv-output-substitution-goal.cc +++ b/src/libstore/build/drv-output-substitution-goal.cc @@ -1,8 +1,9 @@ -#include "drv-output-substitution-goal.hh" -#include "finally.hh" -#include "worker.hh" -#include "substitution-goal.hh" -#include "callback.hh" +#include "nix/store/build/drv-output-substitution-goal.hh" +#include "nix/util/finally.hh" +#include "nix/store/build/worker.hh" +#include "nix/store/build/substitution-goal.hh" +#include "nix/util/callback.hh" +#include "nix/store/store-open.hh" namespace nix { @@ -11,7 +12,7 @@ DrvOutputSubstitutionGoal::DrvOutputSubstitutionGoal( Worker & worker, RepairFlag repair, std::optional ca) - : Goal(worker, DerivedPath::Opaque { StorePath::dummy }) + : Goal(worker, init()) , id(id) { name = fmt("substitution of '%s'", id.to_string()); @@ -87,6 +88,8 @@ Goal::Co DrvOutputSubstitutionGoal::init() bool failed = false; + Goals waitees; + for (const auto & [depId, depPath] : outputInfo->dependentRealisations) { if (depId != id) { if (auto localOutputInfo = worker.store.queryRealisation(depId); @@ -103,13 +106,13 @@ Goal::Co DrvOutputSubstitutionGoal::init() failed = true; break; } - addWaitee(worker.makeDrvOutputSubstitutionGoal(depId)); + waitees.insert(worker.makeDrvOutputSubstitutionGoal(depId)); } } if (failed) continue; - co_return realisationFetched(outputInfo, sub); + co_return realisationFetched(std::move(waitees), outputInfo, sub); } /* None left. Terminate this goal and let someone else deal @@ -127,16 +130,16 @@ Goal::Co DrvOutputSubstitutionGoal::init() co_return amDone(substituterFailed ? ecFailed : ecNoSubstituters); } -Goal::Co DrvOutputSubstitutionGoal::realisationFetched(std::shared_ptr outputInfo, nix::ref sub) { - addWaitee(worker.makePathSubstitutionGoal(outputInfo->outPath)); +Goal::Co DrvOutputSubstitutionGoal::realisationFetched(Goals waitees, std::shared_ptr outputInfo, nix::ref sub) { + waitees.insert(worker.makePathSubstitutionGoal(outputInfo->outPath)); - if (!waitees.empty()) co_await Suspend{}; + co_await await(std::move(waitees)); trace("output path substituted"); if (nrFailed > 0) { debug("The output path of the derivation output '%s' could not be substituted", id.to_string()); - co_return amDone(nrNoSubstituters > 0 || nrIncompleteClosure > 0 ? ecIncompleteClosure : ecFailed); + co_return amDone(nrNoSubstituters > 0 ? ecNoSubstituters : ecFailed); } worker.store.registerDrvOutput(*outputInfo); diff --git a/src/libstore/build/entry-points.cc b/src/libstore/build/entry-points.cc index 3bf22320e3a..6c842554c3b 100644 --- a/src/libstore/build/entry-points.cc +++ b/src/libstore/build/entry-points.cc @@ -1,10 +1,9 @@ -#include "worker.hh" -#include "substitution-goal.hh" -#ifndef _WIN32 // TODO Enable building on Windows -# include "derivation-goal.hh" -#endif -#include "local-store.hh" -#include "strings.hh" +#include "nix/store/derivations.hh" +#include "nix/store/build/worker.hh" +#include "nix/store/build/substitution-goal.hh" +#include "nix/store/build/derivation-trampoline-goal.hh" +#include "nix/store/local-store.hh" +#include "nix/util/strings.hh" namespace nix { @@ -28,12 +27,9 @@ void Store::buildPaths(const std::vector & reqs, BuildMode buildMod ex = std::move(i->ex); } if (i->exitCode != Goal::ecSuccess) { -#ifndef _WIN32 // TODO Enable building on Windows - if (auto i2 = dynamic_cast(i.get())) - failed.insert(printStorePath(i2->drvPath)); - else -#endif - if (auto i2 = dynamic_cast(i.get())) + if (auto i2 = dynamic_cast(i.get())) + failed.insert(i2->drvReq->to_string(*this)); + else if (auto i2 = dynamic_cast(i.get())) failed.insert(printStorePath(i2->storePath)); } } @@ -70,7 +66,7 @@ std::vector Store::buildPathsWithResults( for (auto & [req, goalPtr] : state) results.emplace_back(KeyedBuildResult { - goalPtr->getBuildResult(req), + goalPtr->buildResult, /* .path = */ req, }); @@ -81,19 +77,11 @@ BuildResult Store::buildDerivation(const StorePath & drvPath, const BasicDerivat BuildMode buildMode) { Worker worker(*this, *this); -#ifndef _WIN32 // TODO Enable building on Windows - auto goal = worker.makeBasicDerivationGoal(drvPath, drv, OutputsSpec::All {}, buildMode); -#else - std::shared_ptr goal; - throw UnimplementedError("Building derivations not yet implemented on windows."); -#endif + auto goal = worker.makeDerivationTrampolineGoal(drvPath, OutputsSpec::All {}, drv, buildMode); try { worker.run(Goals{goal}); - return goal->getBuildResult(DerivedPath::Built { - .drvPath = makeConstantStorePathRef(drvPath), - .outputs = OutputsSpec::All {}, - }); + return goal->buildResult; } catch (Error & e) { return BuildResult { .status = BuildResult::MiscFailure, diff --git a/src/libstore/build/goal.cc b/src/libstore/build/goal.cc index 9a16da14555..88b0c28c07e 100644 --- a/src/libstore/build/goal.cc +++ b/src/libstore/build/goal.cc @@ -1,5 +1,5 @@ -#include "goal.hh" -#include "worker.hh" +#include "nix/store/build/goal.hh" +#include "nix/store/build/worker.hh" namespace nix { @@ -101,30 +101,6 @@ bool CompareGoalPtrs::operator() (const GoalPtr & a, const GoalPtr & b) const { return s1 < s2; } - -BuildResult Goal::getBuildResult(const DerivedPath & req) const { - BuildResult res { buildResult }; - - if (auto pbp = std::get_if(&req)) { - auto & bp = *pbp; - - /* Because goals are in general shared between derived paths - that share the same derivation, we need to filter their - results to get back just the results we care about. - */ - - for (auto it = res.builtOutputs.begin(); it != res.builtOutputs.end();) { - if (bp.outputs.contains(it->first)) - ++it; - else - it = res.builtOutputs.erase(it); - } - } - - return res; -} - - void addToWeakGoals(WeakGoals & goals, GoalPtr p) { if (goals.find(p) != goals.end()) @@ -132,38 +108,18 @@ void addToWeakGoals(WeakGoals & goals, GoalPtr p) goals.insert(p); } - -void Goal::addWaitee(GoalPtr waitee) -{ - waitees.insert(waitee); - addToWeakGoals(waitee->waiters, shared_from_this()); -} - - -void Goal::waiteeDone(GoalPtr waitee, ExitCode result) +Co Goal::await(Goals new_waitees) { - assert(waitees.count(waitee)); - waitees.erase(waitee); - - trace(fmt("waitee '%s' done; %d left", waitee->name, waitees.size())); - - if (result == ecFailed || result == ecNoSubstituters || result == ecIncompleteClosure) ++nrFailed; - - if (result == ecNoSubstituters) ++nrNoSubstituters; - - if (result == ecIncompleteClosure) ++nrIncompleteClosure; - - if (waitees.empty() || (result == ecFailed && !settings.keepGoing)) { - - /* If we failed and keepGoing is not set, we remove all - remaining waitees. */ - for (auto & goal : waitees) { - goal->waiters.extract(shared_from_this()); + assert(waitees.empty()); + if (!new_waitees.empty()) { + waitees = std::move(new_waitees); + for (auto waitee : waitees) { + addToWeakGoals(waitee->waiters, shared_from_this()); } - waitees.clear(); - - worker.wakeUp(shared_from_this()); + co_await Suspend{}; + assert(waitees.empty()); } + co_return Return{}; } Goal::Done Goal::amDone(ExitCode result, std::optional ex) @@ -171,11 +127,11 @@ Goal::Done Goal::amDone(ExitCode result, std::optional ex) trace("done"); assert(top_co); assert(exitCode == ecBusy); - assert(result == ecSuccess || result == ecFailed || result == ecNoSubstituters || result == ecIncompleteClosure); + assert(result == ecSuccess || result == ecFailed || result == ecNoSubstituters); exitCode = result; if (ex) { - if (!waiters.empty()) + if (!preserveException && !waiters.empty()) logError(ex->info()); else this->ex = std::move(*ex); @@ -183,7 +139,30 @@ Goal::Done Goal::amDone(ExitCode result, std::optional ex) for (auto & i : waiters) { GoalPtr goal = i.lock(); - if (goal) goal->waiteeDone(shared_from_this(), result); + if (goal) { + auto me = shared_from_this(); + assert(goal->waitees.count(me)); + goal->waitees.erase(me); + + goal->trace(fmt("waitee '%s' done; %d left", name, goal->waitees.size())); + + if (result == ecFailed || result == ecNoSubstituters) ++goal->nrFailed; + + if (result == ecNoSubstituters) ++goal->nrNoSubstituters; + + if (goal->waitees.empty()) { + worker.wakeUp(goal); + } else if (result == ecFailed && !settings.keepGoing) { + /* If we failed and keepGoing is not set, we remove all + remaining waitees. */ + for (auto & g : goal->waitees) { + g->waiters.extract(goal); + } + goal->waitees.clear(); + + worker.wakeUp(goal); + } + } } waiters.clear(); worker.removeGoal(shared_from_this()); @@ -215,5 +194,22 @@ void Goal::work() assert(top_co || exitCode != ecBusy); } +Goal::Co Goal::yield() { + worker.wakeUp(shared_from_this()); + co_await Suspend{}; + co_return Return{}; +} + +Goal::Co Goal::waitForAWhile() { + worker.waitForAWhile(shared_from_this()); + co_await Suspend{}; + co_return Return{}; +} + +Goal::Co Goal::waitForBuildSlot() { + worker.waitForBuildSlot(shared_from_this()); + co_await Suspend{}; + co_return Return{}; +} } diff --git a/src/libstore/build/substitution-goal.cc b/src/libstore/build/substitution-goal.cc index 983c86601d8..9ffc8219d97 100644 --- a/src/libstore/build/substitution-goal.cc +++ b/src/libstore/build/substitution-goal.cc @@ -1,14 +1,15 @@ -#include "worker.hh" -#include "substitution-goal.hh" -#include "nar-info.hh" -#include "finally.hh" -#include "signals.hh" +#include "nix/store/build/worker.hh" +#include "nix/store/store-open.hh" +#include "nix/store/build/substitution-goal.hh" +#include "nix/store/nar-info.hh" +#include "nix/util/finally.hh" +#include "nix/util/signals.hh" #include namespace nix { PathSubstitutionGoal::PathSubstitutionGoal(const StorePath & storePath, Worker & worker, RepairFlag repair, std::optional ca) - : Goal(worker, DerivedPath::Opaque { storePath }) + : Goal(worker, init()) , storePath(storePath) , repair(repair) , ca(ca) @@ -121,20 +122,22 @@ Goal::Co PathSubstitutionGoal::init() /* Bail out early if this substituter lacks a valid signature. LocalStore::addToStore() also checks for this, but only after we've downloaded the path. */ - if (!sub->isTrusted && worker.store.pathInfoIsUntrusted(*info)) + if (!sub->config.isTrusted && worker.store.pathInfoIsUntrusted(*info)) { warn("ignoring substitute for '%s' from '%s', as it's not signed by any of the keys in 'trusted-public-keys'", worker.store.printStorePath(storePath), sub->getUri()); continue; } + Goals waitees; + /* To maintain the closure invariant, we first have to realise the paths referenced by this one. */ for (auto & i : info->references) if (i != storePath) /* ignore self-references */ - addWaitee(worker.makePathSubstitutionGoal(i)); + waitees.insert(worker.makePathSubstitutionGoal(i)); - if (!waitees.empty()) co_await Suspend{}; + co_await await(std::move(waitees)); // FIXME: consider returning boolean instead of passing in reference bool out = false; // is mutated by tryToRun @@ -166,17 +169,21 @@ Goal::Co PathSubstitutionGoal::tryToRun(StorePath subPath, nix::ref sub, if (nrFailed > 0) { co_return done( - nrNoSubstituters > 0 || nrIncompleteClosure > 0 ? ecIncompleteClosure : ecFailed, + nrNoSubstituters > 0 ? ecNoSubstituters : ecFailed, BuildResult::DependencyFailed, fmt("some references of path '%s' could not be realised", worker.store.printStorePath(storePath))); } for (auto & i : info->references) - if (i != storePath) /* ignore self-references */ - assert(worker.store.isValidPath(i)); + /* ignore self-references */ + if (i != storePath) { + if (!worker.store.isValidPath(i)) { + throw Error("reference '%s' of path '%s' is not a valid path", + worker.store.printStorePath(i), worker.store.printStorePath(storePath)); + } + } - worker.wakeUp(shared_from_this()); - co_await Suspend{}; + co_await yield(); trace("trying to run"); @@ -184,8 +191,7 @@ Goal::Co PathSubstitutionGoal::tryToRun(StorePath subPath, nix::ref sub, if maxSubstitutionJobs == 0, we still allow a substituter to run. This prevents infinite waiting. */ while (worker.getNrSubstitutions() >= std::max(1U, (unsigned int) settings.maxSubstitutionJobs)) { - worker.waitForBuildSlot(shared_from_this()); - co_await Suspend{}; + co_await waitForBuildSlot(); } auto maintainRunningSubstitutions = std::make_unique>(worker.runningSubstitutions); @@ -210,7 +216,7 @@ Goal::Co PathSubstitutionGoal::tryToRun(StorePath subPath, nix::ref sub, PushActivity pact(act.id); copyStorePath(*sub, worker.store, - subPath, repair, sub->isTrusted ? NoCheckSigs : CheckSigs); + subPath, repair, sub->config.isTrusted ? NoCheckSigs : CheckSigs); promise.set_value(); } catch (...) { diff --git a/src/libstore/build/worker.cc b/src/libstore/build/worker.cc index dbe86f43f6a..2b901f8189c 100644 --- a/src/libstore/build/worker.cc +++ b/src/libstore/build/worker.cc @@ -1,14 +1,15 @@ -#include "local-store.hh" -#include "machines.hh" -#include "worker.hh" -#include "substitution-goal.hh" -#include "drv-output-substitution-goal.hh" -#include "derivation-goal.hh" +#include "nix/store/local-store.hh" +#include "nix/store/machines.hh" +#include "nix/store/build/worker.hh" +#include "nix/store/build/substitution-goal.hh" +#include "nix/store/build/drv-output-substitution-goal.hh" +#include "nix/store/build/derivation-goal.hh" +#include "nix/store/build/derivation-building-goal.hh" +#include "nix/store/build/derivation-trampoline-goal.hh" #ifndef _WIN32 // TODO Enable building on Windows -# include "local-derivation-goal.hh" -# include "hook-instance.hh" +# include "nix/store/build/hook-instance.hh" #endif -#include "signals.hh" +#include "nix/util/signals.hh" namespace nix { @@ -42,77 +43,63 @@ Worker::~Worker() assert(expectedNarSize == 0); } +template +std::shared_ptr Worker::initGoalIfNeeded(std::weak_ptr & goal_weak, Args && ...args) +{ + if (auto goal = goal_weak.lock()) return goal; + + auto goal = std::make_shared(args...); + goal_weak = goal; + wakeUp(goal); + return goal; +} + +std::shared_ptr Worker::makeDerivationTrampolineGoal( + ref drvReq, + const OutputsSpec & wantedOutputs, + BuildMode buildMode) +{ + return initGoalIfNeeded( + derivationTrampolineGoals.ensureSlot(*drvReq).value[wantedOutputs], + drvReq, wantedOutputs, *this, buildMode); +} -std::shared_ptr Worker::makeDerivationGoalCommon( + +std::shared_ptr Worker::makeDerivationTrampolineGoal( const StorePath & drvPath, const OutputsSpec & wantedOutputs, - std::function()> mkDrvGoal) + const Derivation & drv, + BuildMode buildMode) { - std::weak_ptr & goal_weak = derivationGoals[drvPath]; - std::shared_ptr goal = goal_weak.lock(); - if (!goal) { - goal = mkDrvGoal(); - goal_weak = goal; - wakeUp(goal); - } else { - goal->addWantedOutputs(wantedOutputs); - } - return goal; + return initGoalIfNeeded( + derivationTrampolineGoals.ensureSlot(DerivedPath::Opaque{drvPath}).value[wantedOutputs], + drvPath, wantedOutputs, drv, *this, buildMode); } std::shared_ptr Worker::makeDerivationGoal(const StorePath & drvPath, - const OutputsSpec & wantedOutputs, BuildMode buildMode) + const Derivation & drv, const OutputName & wantedOutput, BuildMode buildMode) { - return makeDerivationGoalCommon(drvPath, wantedOutputs, [&]() -> std::shared_ptr { - return -#ifndef _WIN32 // TODO Enable building on Windows - dynamic_cast(&store) - ? std::make_shared(drvPath, wantedOutputs, *this, buildMode) - : -#endif - std::make_shared(drvPath, wantedOutputs, *this, buildMode); - }); + return initGoalIfNeeded(derivationGoals[drvPath][wantedOutput], drvPath, drv, wantedOutput, *this, buildMode); } -std::shared_ptr Worker::makeBasicDerivationGoal(const StorePath & drvPath, - const BasicDerivation & drv, const OutputsSpec & wantedOutputs, BuildMode buildMode) + +std::shared_ptr Worker::makeDerivationBuildingGoal(const StorePath & drvPath, + const Derivation & drv, BuildMode buildMode) { - return makeDerivationGoalCommon(drvPath, wantedOutputs, [&]() -> std::shared_ptr { - return -#ifndef _WIN32 // TODO Enable building on Windows - dynamic_cast(&store) - ? std::make_shared(drvPath, drv, wantedOutputs, *this, buildMode) - : -#endif - std::make_shared(drvPath, drv, wantedOutputs, *this, buildMode); - }); + return initGoalIfNeeded(derivationBuildingGoals[drvPath], drvPath, drv, *this, buildMode); } std::shared_ptr Worker::makePathSubstitutionGoal(const StorePath & path, RepairFlag repair, std::optional ca) { - std::weak_ptr & goal_weak = substitutionGoals[path]; - auto goal = goal_weak.lock(); // FIXME - if (!goal) { - goal = std::make_shared(path, *this, repair, ca); - goal_weak = goal; - wakeUp(goal); - } - return goal; + return initGoalIfNeeded(substitutionGoals[path], path, *this, repair, ca); } std::shared_ptr Worker::makeDrvOutputSubstitutionGoal(const DrvOutput& id, RepairFlag repair, std::optional ca) { - std::weak_ptr & goal_weak = drvOutputSubstitutionGoals[id]; - auto goal = goal_weak.lock(); // FIXME - if (!goal) { - goal = std::make_shared(id, *this, repair, ca); - goal_weak = goal; - wakeUp(goal); - } - return goal; + return initGoalIfNeeded(drvOutputSubstitutionGoals[id], id, *this, repair, ca); } @@ -120,10 +107,7 @@ GoalPtr Worker::makeGoal(const DerivedPath & req, BuildMode buildMode) { return std::visit(overloaded { [&](const DerivedPath::Built & bfd) -> GoalPtr { - if (auto bop = std::get_if(&*bfd.drvPath)) - return makeDerivationGoal(bop->path, bfd.outputs, buildMode); - else - throw UnimplementedError("Building dynamic derivations in one shot is not yet implemented."); + return makeDerivationTrampolineGoal(bfd.drvPath, bfd.outputs, buildMode); }, [&](const DerivedPath::Opaque & bo) -> GoalPtr { return makePathSubstitutionGoal(bo.path, buildMode == bmRepair ? Repair : NoRepair); @@ -131,28 +115,55 @@ GoalPtr Worker::makeGoal(const DerivedPath & req, BuildMode buildMode) }, req.raw()); } +/** + * This function is polymorphic (both via type parameters and + * overloading) and recursive in order to work on a various types of + * trees + * + * @return Whether the tree node we are processing is not empty / should + * be kept alive. In the case of this overloading the node in question + * is the leaf, the weak reference itself. If the weak reference points + * to the goal we are looking for, our caller can delete it. In the + * inductive case where the node is an interior node, we'll likewise + * return whether the interior node is non-empty. If it is empty + * (because we just deleted its last child), then our caller can + * likewise delete it. + */ +template +static bool removeGoal(std::shared_ptr goal, std::weak_ptr & gp) +{ + return gp.lock() != goal; +} -template -static void removeGoal(std::shared_ptr goal, std::map> & goalMap) +template +static bool removeGoal(std::shared_ptr goal, std::map & goalMap) { /* !!! inefficient */ - for (auto i = goalMap.begin(); - i != goalMap.end(); ) - if (i->second.lock() == goal) { - auto j = i; ++j; - goalMap.erase(i); - i = j; - } - else ++i; + for (auto i = goalMap.begin(); i != goalMap.end();) { + if (!removeGoal(goal, i->second)) + i = goalMap.erase(i); + else + ++i; + } + return !goalMap.empty(); +} + +template +static bool removeGoal(std::shared_ptr goal, typename DerivedPathMap>>::ChildNode & node) +{ + return removeGoal(goal, node.value) || removeGoal(goal, node.childMap); } void Worker::removeGoal(GoalPtr goal) { - if (auto drvGoal = std::dynamic_pointer_cast(goal)) + if (auto drvGoal = std::dynamic_pointer_cast(goal)) + nix::removeGoal(drvGoal, derivationTrampolineGoals.map); + else if (auto drvGoal = std::dynamic_pointer_cast(goal)) nix::removeGoal(drvGoal, derivationGoals); - else - if (auto subGoal = std::dynamic_pointer_cast(goal)) + else if (auto drvBuildingGoal = std::dynamic_pointer_cast(goal)) + nix::removeGoal(drvBuildingGoal, derivationBuildingGoals); + else if (auto subGoal = std::dynamic_pointer_cast(goal)) nix::removeGoal(subGoal, substitutionGoals); else if (auto subGoal = std::dynamic_pointer_cast(goal)) nix::removeGoal(subGoal, drvOutputSubstitutionGoals); @@ -215,6 +226,9 @@ void Worker::childStarted(GoalPtr goal, const std::set 0); nrLocalBuilds--; break; + case JobCategory::Administration: + /* Intentionally not limited, see docs */ + break; default: unreachable(); } @@ -290,21 +307,18 @@ void Worker::run(const Goals & _topGoals) for (auto & i : _topGoals) { topGoals.insert(i); - if (auto goal = dynamic_cast(i.get())) { + if (auto goal = dynamic_cast(i.get())) { topPaths.push_back(DerivedPath::Built { - .drvPath = makeConstantStorePathRef(goal->drvPath), + .drvPath = goal->drvReq, .outputs = goal->wantedOutputs, }); - } else - if (auto goal = dynamic_cast(i.get())) { + } else if (auto goal = dynamic_cast(i.get())) { topPaths.push_back(DerivedPath::Opaque{goal->storePath}); } } /* Call queryMissing() to efficiently query substitutes. */ - StorePathSet willBuild, willSubstitute, unknown; - uint64_t downloadSize, narSize; - store.queryMissing(topPaths, willBuild, willSubstitute, unknown, downloadSize, narSize); + store.queryMissing(topPaths); debug("entered goal loop"); @@ -340,23 +354,14 @@ void Worker::run(const Goals & _topGoals) else if (awake.empty() && 0U == settings.maxBuildJobs) { if (getMachines().empty()) throw Error( - R"( - Unable to start any build; - either increase '--max-jobs' or enable remote builds. - - For more information run 'man nix.conf' and search for '/machines'. - )" - ); + "Unable to start any build; either increase '--max-jobs' or enable remote builds.\n" + "\n" + "For more information run 'man nix.conf' and search for '/machines'."); else throw Error( - R"( - Unable to start any build; - remote machines may not have all required system features. - - For more information run 'man nix.conf' and search for '/machines'. - )" - ); - + "Unable to start any build; remote machines may not have all required system features.\n" + "\n" + "For more information run 'man nix.conf' and search for '/machines'."); } else assert(!awake.empty()); } @@ -524,7 +529,7 @@ bool Worker::pathContentsGood(const StorePath & path) res = false; else { auto current = hashPath( - {store.getFSAccessor(), CanonPath(store.printStorePath(path))}, + {store.getFSAccessor(), CanonPath(path.to_string())}, FileIngestionMethod::NixArchive, info->narHash.algo).first; Hash nullHash(HashAlgorithm::SHA256); res = info->narHash == nullHash || info->narHash == current; @@ -552,4 +557,9 @@ GoalPtr upcast_goal(std::shared_ptr subGoal) return subGoal; } +GoalPtr upcast_goal(std::shared_ptr subGoal) +{ + return subGoal; +} + } diff --git a/src/libstore/builtins.hh b/src/libstore/builtins.hh deleted file mode 100644 index 091946e013a..00000000000 --- a/src/libstore/builtins.hh +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once -///@file - -#include "derivations.hh" - -namespace nix { - -// TODO: make pluggable. -void builtinFetchurl( - const BasicDerivation & drv, - const std::map & outputs, - const std::string & netrcData, - const std::string & caFileData); - -void builtinUnpackChannel( - const BasicDerivation & drv, - const std::map & outputs); - -} diff --git a/src/libstore/builtins/buildenv.cc b/src/libstore/builtins/buildenv.cc index 0f7bcd99b1c..0e99ca0e56d 100644 --- a/src/libstore/builtins/buildenv.cc +++ b/src/libstore/builtins/buildenv.cc @@ -1,6 +1,7 @@ -#include "buildenv.hh" -#include "derivations.hh" -#include "signals.hh" +#include "nix/store/builtins/buildenv.hh" +#include "nix/store/builtins.hh" +#include "nix/store/derivations.hh" +#include "nix/util/signals.hh" #include #include @@ -18,12 +19,12 @@ struct State /* For each activated package, create symlinks */ static void createLinks(State & state, const Path & srcDir, const Path & dstDir, int priority) { - std::filesystem::directory_iterator srcFiles; + DirectoryIterator srcFiles; try { - srcFiles = std::filesystem::directory_iterator{srcDir}; - } catch (std::filesystem::filesystem_error & e) { - if (e.code() == std::errc::not_a_directory) { + srcFiles = DirectoryIterator{srcDir}; + } catch (SysError & e) { + if (e.errNo == ENOTDIR) { warn("not including '%s' in the user environment because it's not a directory", srcDir); return; } @@ -123,7 +124,7 @@ void buildProfile(const Path & out, Packages && pkgs) { State state; - std::set done, postponed; + PathSet done, postponed; auto addPkg = [&](const Path & pkgDir, int priority) { if (!done.insert(pkgDir).second) return; @@ -157,7 +158,7 @@ void buildProfile(const Path & out, Packages && pkgs) */ auto priorityCounter = 1000; while (!postponed.empty()) { - std::set pkgDirs; + PathSet pkgDirs; postponed.swap(pkgDirs); for (const auto & pkgDir : pkgDirs) addPkg(pkgDir, priorityCounter++); @@ -166,17 +167,15 @@ void buildProfile(const Path & out, Packages && pkgs) debug("created %d symlinks in user environment", state.symlinks); } -void builtinBuildenv( - const BasicDerivation & drv, - const std::map & outputs) +static void builtinBuildenv(const BuiltinBuilderContext & ctx) { auto getAttr = [&](const std::string & name) { - auto i = drv.env.find(name); - if (i == drv.env.end()) throw Error("attribute '%s' missing", name); + auto i = ctx.drv.env.find(name); + if (i == ctx.drv.env.end()) throw Error("attribute '%s' missing", name); return i->second; }; - auto out = outputs.at("out"); + auto out = ctx.outputs.at("out"); createDirs(out); /* Convert the stuff we get from the environment back into a @@ -203,4 +202,6 @@ void builtinBuildenv( createSymlink(getAttr("manifest"), out + "/manifest.nix"); } +static RegisterBuiltinBuilder registerBuildenv("buildenv", builtinBuildenv); + } diff --git a/src/libstore/builtins/fetchurl.cc b/src/libstore/builtins/fetchurl.cc index 90e58dfdb3d..18fa755580f 100644 --- a/src/libstore/builtins/fetchurl.cc +++ b/src/libstore/builtins/fetchurl.cc @@ -1,38 +1,34 @@ -#include "builtins.hh" -#include "filetransfer.hh" -#include "store-api.hh" -#include "archive.hh" -#include "compression.hh" +#include "nix/store/builtins.hh" +#include "nix/store/filetransfer.hh" +#include "nix/store/store-api.hh" +#include "nix/util/archive.hh" +#include "nix/util/compression.hh" namespace nix { -void builtinFetchurl( - const BasicDerivation & drv, - const std::map & outputs, - const std::string & netrcData, - const std::string & caFileData) +static void builtinFetchurl(const BuiltinBuilderContext & ctx) { /* Make the host's netrc data available. Too bad curl requires this to be stored in a file. It would be nice if we could just pass a pointer to the data. */ - if (netrcData != "") { + if (ctx.netrcData != "") { settings.netrcFile = "netrc"; - writeFile(settings.netrcFile, netrcData, 0600); + writeFile(settings.netrcFile, ctx.netrcData, 0600); } settings.caFile = "ca-certificates.crt"; - writeFile(settings.caFile, caFileData, 0600); + writeFile(settings.caFile, ctx.caFileData, 0600); - auto out = get(drv.outputs, "out"); + auto out = get(ctx.drv.outputs, "out"); if (!out) throw Error("'builtin:fetchurl' requires an 'out' output"); - if (!(drv.type().isFixed() || drv.type().isImpure())) + if (!(ctx.drv.type().isFixed() || ctx.drv.type().isImpure())) throw Error("'builtin:fetchurl' must be a fixed-output or impure derivation"); - auto storePath = outputs.at("out"); - auto mainUrl = drv.env.at("url"); - bool unpack = getOr(drv.env, "unpack", "") == "1"; + auto storePath = ctx.outputs.at("out"); + auto mainUrl = ctx.drv.env.at("url"); + bool unpack = getOr(ctx.drv.env, "unpack", "") == "1"; /* Note: have to use a fresh fileTransfer here because we're in a forked process. */ @@ -56,8 +52,8 @@ void builtinFetchurl( else writeFile(storePath, *source); - auto executable = drv.env.find("executable"); - if (executable != drv.env.end() && executable->second == "1") { + auto executable = ctx.drv.env.find("executable"); + if (executable != ctx.drv.env.end() && executable->second == "1") { if (chmod(storePath.c_str(), 0755) == -1) throw SysError("making '%1%' executable", storePath); } @@ -79,4 +75,6 @@ void builtinFetchurl( fetch(mainUrl); } +static RegisterBuiltinBuilder registerFetchurl("fetchurl", builtinFetchurl); + } diff --git a/src/libstore/builtins/unpack-channel.cc b/src/libstore/builtins/unpack-channel.cc index a6369ee1c8c..dd6b8bb71e4 100644 --- a/src/libstore/builtins/unpack-channel.cc +++ b/src/libstore/builtins/unpack-channel.cc @@ -1,55 +1,45 @@ -#include "builtins.hh" -#include "tarfile.hh" +#include "nix/store/builtins.hh" +#include "nix/util/tarfile.hh" namespace nix { -namespace fs { using namespace std::filesystem; } - -void builtinUnpackChannel( - const BasicDerivation & drv, - const std::map & outputs) +static void builtinUnpackChannel(const BuiltinBuilderContext & ctx) { auto getAttr = [&](const std::string & name) -> const std::string & { - auto i = drv.env.find(name); - if (i == drv.env.end()) throw Error("attribute '%s' missing", name); + auto i = ctx.drv.env.find(name); + if (i == ctx.drv.env.end()) throw Error("attribute '%s' missing", name); return i->second; }; - fs::path out{outputs.at("out")}; + std::filesystem::path out{ctx.outputs.at("out")}; auto & channelName = getAttr("channelName"); auto & src = getAttr("src"); - if (fs::path{channelName}.filename().string() != channelName) { + if (std::filesystem::path{channelName}.filename().string() != channelName) { throw Error("channelName is not allowed to contain filesystem separators, got %1%", channelName); } - try { - fs::create_directories(out); - } catch (fs::filesystem_error &) { - throw SysError("creating directory '%1%'", out.string()); - } + createDirs(out); unpackTarfile(src, out); size_t fileCount; std::string fileName; - try { - auto entries = fs::directory_iterator{out}; - fileName = entries->path().string(); - fileCount = std::distance(fs::begin(entries), fs::end(entries)); - } catch (fs::filesystem_error &) { - throw SysError("failed to read directory %1%", out.string()); - } + auto entries = DirectoryIterator{out}; + fileName = entries->path().string(); + fileCount = std::distance(entries.begin(), entries.end()); if (fileCount != 1) throw Error("channel tarball '%s' contains more than one file", src); auto target = out / channelName; try { - fs::rename(fileName, target); - } catch (fs::filesystem_error &) { + std::filesystem::rename(fileName, target); + } catch (std::filesystem::filesystem_error &) { throw SysError("failed to rename %1% to %2%", fileName, target.string()); } } +static RegisterBuiltinBuilder registerUnpackChannel("unpack-channel", builtinUnpackChannel); + } diff --git a/src/libstore/ca-specific-schema.sql b/src/libstore/ca-specific-schema.sql index 4ca91f58544..c5e4e389799 100644 --- a/src/libstore/ca-specific-schema.sql +++ b/src/libstore/ca-specific-schema.sql @@ -1,4 +1,4 @@ --- Extension of the sql schema for content-addressed derivations. +-- Extension of the sql schema for content-addressing derivations. -- Won't be loaded unless the experimental feature `ca-derivations` -- is enabled diff --git a/src/libstore/common-protocol.cc b/src/libstore/common-protocol.cc index fc2b5ac6f3f..311f4888c66 100644 --- a/src/libstore/common-protocol.cc +++ b/src/libstore/common-protocol.cc @@ -1,11 +1,11 @@ -#include "serialise.hh" -#include "path-with-outputs.hh" -#include "store-api.hh" -#include "build-result.hh" -#include "common-protocol.hh" -#include "common-protocol-impl.hh" -#include "archive.hh" -#include "derivations.hh" +#include "nix/util/serialise.hh" +#include "nix/store/path-with-outputs.hh" +#include "nix/store/store-api.hh" +#include "nix/store/build-result.hh" +#include "nix/store/common-protocol.hh" +#include "nix/store/common-protocol-impl.hh" +#include "nix/util/archive.hh" +#include "nix/store/derivations.hh" #include diff --git a/src/libstore/common-ssh-store-config.cc b/src/libstore/common-ssh-store-config.cc index 05332b9bb5c..bcaa11a9671 100644 --- a/src/libstore/common-ssh-store-config.cc +++ b/src/libstore/common-ssh-store-config.cc @@ -1,7 +1,7 @@ #include -#include "common-ssh-store-config.hh" -#include "ssh.hh" +#include "nix/store/common-ssh-store-config.hh" +#include "nix/store/ssh.hh" namespace nix { @@ -28,7 +28,7 @@ CommonSSHStoreConfig::CommonSSHStoreConfig(std::string_view scheme, std::string_ { } -SSHMaster CommonSSHStoreConfig::createSSHMaster(bool useMaster, Descriptor logFD) +SSHMaster CommonSSHStoreConfig::createSSHMaster(bool useMaster, Descriptor logFD) const { return { host, diff --git a/src/libstore/content-address.cc b/src/libstore/content-address.cc index e1cdfece6e9..5d27c41367f 100644 --- a/src/libstore/content-address.cc +++ b/src/libstore/content-address.cc @@ -1,6 +1,6 @@ -#include "args.hh" -#include "content-address.hh" -#include "split.hh" +#include "nix/util/args.hh" +#include "nix/store/content-address.hh" +#include "nix/util/split.hh" namespace nix { diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index b921dbe2de8..bf4a9d95906 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -1,23 +1,24 @@ -#include "daemon.hh" -#include "signals.hh" -#include "worker-protocol.hh" -#include "worker-protocol-connection.hh" -#include "worker-protocol-impl.hh" -#include "build-result.hh" -#include "store-api.hh" -#include "store-cast.hh" -#include "gc-store.hh" -#include "log-store.hh" -#include "indirect-root-store.hh" -#include "path-with-outputs.hh" -#include "finally.hh" -#include "archive.hh" -#include "derivations.hh" -#include "args.hh" -#include "git.hh" +#include "nix/store/daemon.hh" +#include "nix/util/signals.hh" +#include "nix/store/worker-protocol.hh" +#include "nix/store/worker-protocol-connection.hh" +#include "nix/store/worker-protocol-impl.hh" +#include "nix/store/build-result.hh" +#include "nix/store/store-api.hh" +#include "nix/store/store-cast.hh" +#include "nix/store/gc-store.hh" +#include "nix/store/log-store.hh" +#include "nix/store/indirect-root-store.hh" +#include "nix/store/path-with-outputs.hh" +#include "nix/util/finally.hh" +#include "nix/util/archive.hh" +#include "nix/store/derivations.hh" +#include "nix/util/args.hh" +#include "nix/util/git.hh" +#include "nix/util/logging.hh" #ifndef _WIN32 // TODO need graceful async exit support on Windows? -# include "monitor-fd.hh" +# include "nix/util/monitor-fd.hh" #endif #include @@ -593,7 +594,7 @@ static void performOp(TunnelLogger * logger, ref store, auto drvType = drv.type(); - /* Content-addressed derivations are trustless because their output paths + /* Content-addressing derivations are trustless because their output paths are verified by their content alone, so any derivation is free to try to produce such a path. @@ -947,14 +948,12 @@ static void performOp(TunnelLogger * logger, ref store, case WorkerProto::Op::QueryMissing: { auto targets = WorkerProto::Serialise::read(*store, rconn); logger->startWork(); - StorePathSet willBuild, willSubstitute, unknown; - uint64_t downloadSize, narSize; - store->queryMissing(targets, willBuild, willSubstitute, unknown, downloadSize, narSize); + auto missing = store->queryMissing(targets); logger->stopWork(); - WorkerProto::write(*store, wconn, willBuild); - WorkerProto::write(*store, wconn, willSubstitute); - WorkerProto::write(*store, wconn, unknown); - conn.to << downloadSize << narSize; + WorkerProto::write(*store, wconn, missing.willBuild); + WorkerProto::write(*store, wconn, missing.willSubstitute); + WorkerProto::write(*store, wconn, missing.unknown); + conn.to << missing.downloadSize << missing.narSize; break; } @@ -1025,6 +1024,7 @@ void processConnection( { #ifndef _WIN32 // TODO need graceful async exit support on Windows? auto monitor = !recursive ? std::make_unique(from.fd) : nullptr; + (void) monitor; // suppress warning #endif /* Exchange the greeting. */ @@ -1041,11 +1041,16 @@ void processConnection( conn.protoVersion = protoVersion; conn.features = features; - auto tunnelLogger = new TunnelLogger(conn.to, protoVersion); - auto prevLogger = nix::logger; + auto tunnelLogger_ = std::make_unique(conn.to, protoVersion); + auto tunnelLogger = tunnelLogger_.get(); + std::unique_ptr prevLogger_; + auto prevLogger = logger.get(); // FIXME - if (!recursive) - logger = tunnelLogger; + if (!recursive) { + prevLogger_ = std::move(logger); + logger = std::move(tunnelLogger_); + applyJSONLogger(); + } unsigned int opCount = 0; diff --git a/src/libstore/derivation-options.cc b/src/libstore/derivation-options.cc new file mode 100644 index 00000000000..40c4e6d1547 --- /dev/null +++ b/src/libstore/derivation-options.cc @@ -0,0 +1,384 @@ +#include "nix/store/derivation-options.hh" +#include "nix/util/json-utils.hh" +#include "nix/store/parsed-derivations.hh" +#include "nix/store/derivations.hh" +#include "nix/store/store-api.hh" +#include "nix/util/types.hh" +#include "nix/util/util.hh" + +#include +#include +#include +#include + +namespace nix { + +static std::optional +getStringAttr(const StringMap & env, const StructuredAttrs * parsed, const std::string & name) +{ + if (parsed) { + auto i = parsed->structuredAttrs.find(name); + if (i == parsed->structuredAttrs.end()) + return {}; + else { + if (!i->is_string()) + throw Error("attribute '%s' of must be a string", name); + return i->get(); + } + } else { + auto i = env.find(name); + if (i == env.end()) + return {}; + else + return i->second; + } +} + +static bool getBoolAttr(const StringMap & env, const StructuredAttrs * parsed, const std::string & name, bool def) +{ + if (parsed) { + auto i = parsed->structuredAttrs.find(name); + if (i == parsed->structuredAttrs.end()) + return def; + else { + if (!i->is_boolean()) + throw Error("attribute '%s' must be a Boolean", name); + return i->get(); + } + } else { + auto i = env.find(name); + if (i == env.end()) + return def; + else + return i->second == "1"; + } +} + +static std::optional +getStringsAttr(const StringMap & env, const StructuredAttrs * parsed, const std::string & name) +{ + if (parsed) { + auto i = parsed->structuredAttrs.find(name); + if (i == parsed->structuredAttrs.end()) + return {}; + else { + if (!i->is_array()) + throw Error("attribute '%s' must be a list of strings", name); + Strings res; + for (auto j = i->begin(); j != i->end(); ++j) { + if (!j->is_string()) + throw Error("attribute '%s' must be a list of strings", name); + res.push_back(j->get()); + } + return res; + } + } else { + auto i = env.find(name); + if (i == env.end()) + return {}; + else + return tokenizeString(i->second); + } +} + +static std::optional +getStringSetAttr(const StringMap & env, const StructuredAttrs * parsed, const std::string & name) +{ + auto ss = getStringsAttr(env, parsed, name); + return ss ? (std::optional{StringSet{ss->begin(), ss->end()}}) : (std::optional{}); +} + +using OutputChecks = DerivationOptions::OutputChecks; + +using OutputChecksVariant = std::variant>; + +DerivationOptions +DerivationOptions::fromStructuredAttrs(const StringMap & env, const StructuredAttrs * parsed, bool shouldWarn) +{ + DerivationOptions defaults = {}; + + if (shouldWarn && parsed) { + if (get(parsed->structuredAttrs, "allowedReferences")) { + warn( + "'structuredAttrs' disables the effect of the top-level attribute 'allowedReferences'; use 'outputChecks' instead"); + } + if (get(parsed->structuredAttrs, "allowedRequisites")) { + warn( + "'structuredAttrs' disables the effect of the top-level attribute 'allowedRequisites'; use 'outputChecks' instead"); + } + if (get(parsed->structuredAttrs, "disallowedRequisites")) { + warn( + "'structuredAttrs' disables the effect of the top-level attribute 'disallowedRequisites'; use 'outputChecks' instead"); + } + if (get(parsed->structuredAttrs, "disallowedReferences")) { + warn( + "'structuredAttrs' disables the effect of the top-level attribute 'disallowedReferences'; use 'outputChecks' instead"); + } + if (get(parsed->structuredAttrs, "maxSize")) { + warn( + "'structuredAttrs' disables the effect of the top-level attribute 'maxSize'; use 'outputChecks' instead"); + } + if (get(parsed->structuredAttrs, "maxClosureSize")) { + warn( + "'structuredAttrs' disables the effect of the top-level attribute 'maxClosureSize'; use 'outputChecks' instead"); + } + } + + return { + .outputChecks = [&]() -> OutputChecksVariant { + if (parsed) { + std::map res; + if (auto outputChecks = get(parsed->structuredAttrs, "outputChecks")) { + for (auto & [outputName, output] : getObject(*outputChecks)) { + OutputChecks checks; + + if (auto maxSize = get(output, "maxSize")) + checks.maxSize = maxSize->get(); + + if (auto maxClosureSize = get(output, "maxClosureSize")) + checks.maxClosureSize = maxClosureSize->get(); + + auto get_ = [&output = output](const std::string & name) -> std::optional { + if (auto i = get(output, name)) { + StringSet res; + for (auto j = i->begin(); j != i->end(); ++j) { + if (!j->is_string()) + throw Error("attribute '%s' must be a list of strings", name); + res.insert(j->get()); + } + return res; + } + return {}; + }; + + checks.allowedReferences = get_("allowedReferences"); + checks.allowedRequisites = get_("allowedRequisites"); + checks.disallowedReferences = get_("disallowedReferences").value_or(StringSet{}); + checks.disallowedRequisites = get_("disallowedRequisites").value_or(StringSet{}); + ; + + res.insert_or_assign(outputName, std::move(checks)); + } + } + return res; + } else { + return OutputChecks{ + // legacy non-structured-attributes case + .ignoreSelfRefs = true, + .allowedReferences = getStringSetAttr(env, parsed, "allowedReferences"), + .disallowedReferences = getStringSetAttr(env, parsed, "disallowedReferences").value_or(StringSet{}), + .allowedRequisites = getStringSetAttr(env, parsed, "allowedRequisites"), + .disallowedRequisites = getStringSetAttr(env, parsed, "disallowedRequisites").value_or(StringSet{}), + }; + } + }(), + .unsafeDiscardReferences = + [&] { + std::map res; + + if (parsed) { + if (auto udr = get(parsed->structuredAttrs, "unsafeDiscardReferences")) { + for (auto & [outputName, output] : getObject(*udr)) { + if (!output.is_boolean()) + throw Error("attribute 'unsafeDiscardReferences.\"%s\"' must be a Boolean", outputName); + res.insert_or_assign(outputName, output.get()); + } + } + } + + return res; + }(), + .passAsFile = + [&] { + StringSet res; + if (auto * passAsFileString = get(env, "passAsFile")) { + if (parsed) { + if (shouldWarn) { + warn( + "'structuredAttrs' disables the effect of the top-level attribute 'passAsFile'; because all JSON is always passed via file"); + } + } else { + res = tokenizeString(*passAsFileString); + } + } + return res; + }(), + .exportReferencesGraph = + [&] { + std::map ret; + + if (parsed) { + auto e = optionalValueAt(parsed->structuredAttrs, "exportReferencesGraph"); + if (!e || !e->is_object()) + return ret; + for (auto & [key, value] : getObject(*e)) { + if (value.is_array()) + ret.insert_or_assign(key, value); + else if (value.is_string()) + ret.insert_or_assign(key, StringSet{value}); + else + throw Error("'exportReferencesGraph' value is not an array or a string"); + } + } else { + auto s = getOr(env, "exportReferencesGraph", ""); + Strings ss = tokenizeString(s); + if (ss.size() % 2 != 0) + throw Error("odd number of tokens in 'exportReferencesGraph': '%1%'", s); + for (Strings::iterator i = ss.begin(); i != ss.end();) { + auto fileName = std::move(*i++); + static std::regex regex("[A-Za-z_][A-Za-z0-9_.-]*"); + if (!std::regex_match(fileName, regex)) + throw Error("invalid file name '%s' in 'exportReferencesGraph'", fileName); + + auto & storePathS = *i++; + ret.insert_or_assign(std::move(fileName), StringSet{storePathS}); + } + } + return ret; + }(), + .additionalSandboxProfile = + getStringAttr(env, parsed, "__sandboxProfile").value_or(defaults.additionalSandboxProfile), + .noChroot = getBoolAttr(env, parsed, "__noChroot", defaults.noChroot), + .impureHostDeps = getStringSetAttr(env, parsed, "__impureHostDeps").value_or(defaults.impureHostDeps), + .impureEnvVars = getStringSetAttr(env, parsed, "impureEnvVars").value_or(defaults.impureEnvVars), + .allowLocalNetworking = getBoolAttr(env, parsed, "__darwinAllowLocalNetworking", defaults.allowLocalNetworking), + .requiredSystemFeatures = + getStringSetAttr(env, parsed, "requiredSystemFeatures").value_or(defaults.requiredSystemFeatures), + .preferLocalBuild = getBoolAttr(env, parsed, "preferLocalBuild", defaults.preferLocalBuild), + .allowSubstitutes = getBoolAttr(env, parsed, "allowSubstitutes", defaults.allowSubstitutes), + }; +} + +StringSet DerivationOptions::getRequiredSystemFeatures(const BasicDerivation & drv) const +{ + // FIXME: cache this? + StringSet res; + for (auto & i : requiredSystemFeatures) + res.insert(i); + if (!drv.type().hasKnownOutputPaths()) + res.insert("ca-derivations"); + return res; +} + +bool DerivationOptions::canBuildLocally(Store & localStore, const BasicDerivation & drv) const +{ + if (drv.platform != settings.thisSystem.get() && !settings.extraPlatforms.get().count(drv.platform) + && !drv.isBuiltin()) + return false; + + if (settings.maxBuildJobs.get() == 0 && !drv.isBuiltin()) + return false; + + for (auto & feature : getRequiredSystemFeatures(drv)) + if (!localStore.config.systemFeatures.get().count(feature)) + return false; + + return true; +} + +bool DerivationOptions::willBuildLocally(Store & localStore, const BasicDerivation & drv) const +{ + return preferLocalBuild && canBuildLocally(localStore, drv); +} + +bool DerivationOptions::substitutesAllowed() const +{ + return settings.alwaysAllowSubstitutes ? true : allowSubstitutes; +} + +bool DerivationOptions::useUidRange(const BasicDerivation & drv) const +{ + return getRequiredSystemFeatures(drv).count("uid-range"); +} + +} + +namespace nlohmann { + +using namespace nix; + +DerivationOptions adl_serializer::from_json(const json & json) +{ + return { + .outputChecks = [&]() -> OutputChecksVariant { + auto outputChecks = getObject(valueAt(json, "outputChecks")); + + auto forAllOutputsOpt = optionalValueAt(outputChecks, "forAllOutputs"); + auto perOutputOpt = optionalValueAt(outputChecks, "perOutput"); + + if (forAllOutputsOpt && !perOutputOpt) { + return static_cast(*forAllOutputsOpt); + } else if (perOutputOpt && !forAllOutputsOpt) { + return static_cast>(*perOutputOpt); + } else { + throw Error("Exactly one of 'perOutput' or 'forAllOutputs' is required"); + } + }(), + + .unsafeDiscardReferences = valueAt(json, "unsafeDiscardReferences"), + .passAsFile = getStringSet(valueAt(json, "passAsFile")), + + .additionalSandboxProfile = getString(valueAt(json, "additionalSandboxProfile")), + .noChroot = getBoolean(valueAt(json, "noChroot")), + .impureHostDeps = getStringSet(valueAt(json, "impureHostDeps")), + .impureEnvVars = getStringSet(valueAt(json, "impureEnvVars")), + .allowLocalNetworking = getBoolean(valueAt(json, "allowLocalNetworking")), + + .requiredSystemFeatures = getStringSet(valueAt(json, "requiredSystemFeatures")), + .preferLocalBuild = getBoolean(valueAt(json, "preferLocalBuild")), + .allowSubstitutes = getBoolean(valueAt(json, "allowSubstitutes")), + }; +} + +void adl_serializer::to_json(json & json, DerivationOptions o) +{ + json["outputChecks"] = std::visit( + overloaded{ + [&](const OutputChecks & checks) { + nlohmann::json outputChecks; + outputChecks["forAllOutputs"] = checks; + return outputChecks; + }, + [&](const std::map & checksPerOutput) { + nlohmann::json outputChecks; + outputChecks["perOutput"] = checksPerOutput; + return outputChecks; + }, + }, + o.outputChecks); + + json["unsafeDiscardReferences"] = o.unsafeDiscardReferences; + json["passAsFile"] = o.passAsFile; + + json["additionalSandboxProfile"] = o.additionalSandboxProfile; + json["noChroot"] = o.noChroot; + json["impureHostDeps"] = o.impureHostDeps; + json["impureEnvVars"] = o.impureEnvVars; + json["allowLocalNetworking"] = o.allowLocalNetworking; + + json["requiredSystemFeatures"] = o.requiredSystemFeatures; + json["preferLocalBuild"] = o.preferLocalBuild; + json["allowSubstitutes"] = o.allowSubstitutes; +} + +DerivationOptions::OutputChecks adl_serializer::from_json(const json & json) +{ + return { + .ignoreSelfRefs = getBoolean(valueAt(json, "ignoreSelfRefs")), + .allowedReferences = nullableValueAt(json, "allowedReferences"), + .disallowedReferences = getStringSet(valueAt(json, "disallowedReferences")), + .allowedRequisites = nullableValueAt(json, "allowedRequisites"), + .disallowedRequisites = getStringSet(valueAt(json, "disallowedRequisites")), + }; +} + +void adl_serializer::to_json(json & json, DerivationOptions::OutputChecks c) +{ + json["ignoreSelfRefs"] = c.ignoreSelfRefs; + json["allowedReferences"] = c.allowedReferences; + json["disallowedReferences"] = c.disallowedReferences; + json["allowedRequisites"] = c.allowedRequisites; + json["disallowedRequisites"] = c.disallowedRequisites; +} + +} diff --git a/src/libstore/derivations.cc b/src/libstore/derivations.cc index 1f37b0c384c..0657a749901 100644 --- a/src/libstore/derivations.cc +++ b/src/libstore/derivations.cc @@ -1,14 +1,14 @@ -#include "derivations.hh" -#include "downstream-placeholder.hh" -#include "store-api.hh" -#include "globals.hh" -#include "types.hh" -#include "util.hh" -#include "split.hh" -#include "common-protocol.hh" -#include "common-protocol-impl.hh" -#include "strings-inline.hh" -#include "json-utils.hh" +#include "nix/store/derivations.hh" +#include "nix/store/downstream-placeholder.hh" +#include "nix/store/store-api.hh" +#include "nix/store/globals.hh" +#include "nix/util/types.hh" +#include "nix/util/util.hh" +#include "nix/util/split.hh" +#include "nix/store/common-protocol.hh" +#include "nix/store/common-protocol-impl.hh" +#include "nix/util/strings-inline.hh" +#include "nix/util/json-utils.hh" #include #include @@ -300,7 +300,7 @@ static DerivationOutput parseDerivationOutput( } else { xpSettings.require(Xp::CaDerivations); if (pathS != "") - throw FormatError("content-addressed derivation output should not specify output path"); + throw FormatError("content-addressing derivation output should not specify output path"); return DerivationOutput::CAFloating { .method = std::move(method), .hashAlgo = std::move(hashAlgo), @@ -412,7 +412,7 @@ Derivation parseDerivation( expect(str, "rvWithVersion("); auto versionS = parseString(str); if (*versionS == "xp-dyn-drv") { - // Only verison we have so far + // Only version we have so far version = DerivationATermVersion::DynamicDerivations; xpSettings.require(Xp::DynamicDerivations); } else { @@ -553,7 +553,7 @@ static void unparseDerivedPathMapNode(const StoreDirConfig & store, std::string * derivation? * * In other words, does it on the output of derivation that is itself an - * ouput of a derivation? This corresponds to a dependency that is an + * output of a derivation? This corresponds to a dependency that is an * inductive derived path with more than one layer of * `DerivedPath::Built`. */ @@ -843,16 +843,6 @@ DrvHash hashDerivationModulo(Store & store, const Derivation & drv, bool maskOut }; } - if (type.isImpure()) { - std::map outputHashes; - for (const auto & [outputName, _] : drv.outputs) - outputHashes.insert_or_assign(outputName, impureOutputHash); - return DrvHash { - .hashes = outputHashes, - .kind = DrvHash::Kind::Deferred, - }; - } - auto kind = std::visit(overloaded { [](const DerivationType::InputAddressed & ia) { /* This might be a "pesimistically" deferred output, so we don't @@ -865,7 +855,7 @@ DrvHash hashDerivationModulo(Store & store, const Derivation & drv, bool maskOut : DrvHash::Kind::Deferred; }, [](const DerivationType::Impure &) -> DrvHash::Kind { - assert(false); + return DrvHash::Kind::Deferred; } }, drv.type().raw); @@ -1062,49 +1052,36 @@ static void rewriteDerivation(Store & store, BasicDerivation & drv, const String std::optional Derivation::tryResolve(Store & store, Store * evalStore) const { - std::map, StorePath> inputDrvOutputs; - - std::function::ChildNode &)> accum; - accum = [&](auto & inputDrv, auto & node) { - for (auto & [outputName, outputPath] : store.queryPartialDerivationOutputMap(inputDrv, evalStore)) { - if (outputPath) { - inputDrvOutputs.insert_or_assign({inputDrv, outputName}, *outputPath); - if (auto p = get(node.childMap, outputName)) - accum(*outputPath, *p); + return tryResolve( + store, + [&](ref drvPath, const std::string & outputName) -> std::optional { + try { + return resolveDerivedPath(store, SingleDerivedPath::Built{drvPath, outputName}, evalStore); + } catch (Error &) { + return std::nullopt; } - } - }; - - for (auto & [inputDrv, node] : inputDrvs.map) - accum(inputDrv, node); - - return tryResolve(store, inputDrvOutputs); + }); } static bool tryResolveInput( Store & store, StorePathSet & inputSrcs, StringMap & inputRewrites, const DownstreamPlaceholder * placeholderOpt, - const StorePath & inputDrv, const DerivedPathMap::ChildNode & inputNode, - const std::map, StorePath> & inputDrvOutputs) + ref drvPath, const DerivedPathMap::ChildNode & inputNode, + std::function(ref drvPath, const std::string & outputName)> queryResolutionChain) { - auto getOutput = [&](const std::string & outputName) { - auto * actualPathOpt = get(inputDrvOutputs, { inputDrv, outputName }); - if (!actualPathOpt) - warn("output %s of input %s missing, aborting the resolving", - outputName, - store.printStorePath(inputDrv) - ); - return actualPathOpt; - }; - auto getPlaceholder = [&](const std::string & outputName) { return placeholderOpt ? DownstreamPlaceholder::unknownDerivation(*placeholderOpt, outputName) - : DownstreamPlaceholder::unknownCaOutput(inputDrv, outputName); + : [&]{ + auto * p = std::get_if(&drvPath->raw()); + // otherwise we should have had a placeholder to build-upon already + assert(p); + return DownstreamPlaceholder::unknownCaOutput(p->path, outputName); + }(); }; for (auto & outputName : inputNode.value) { - auto actualPathOpt = getOutput(outputName); + auto actualPathOpt = queryResolutionChain(drvPath, outputName); if (!actualPathOpt) return false; auto actualPath = *actualPathOpt; if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations)) { @@ -1116,13 +1093,12 @@ static bool tryResolveInput( } for (auto & [outputName, childNode] : inputNode.childMap) { - auto actualPathOpt = getOutput(outputName); - if (!actualPathOpt) return false; - auto actualPath = *actualPathOpt; auto nextPlaceholder = getPlaceholder(outputName); if (!tryResolveInput(store, inputSrcs, inputRewrites, - &nextPlaceholder, actualPath, childNode, - inputDrvOutputs)) + &nextPlaceholder, + make_ref(SingleDerivedPath::Built{drvPath, outputName}), + childNode, + queryResolutionChain)) return false; } return true; @@ -1130,7 +1106,7 @@ static bool tryResolveInput( std::optional Derivation::tryResolve( Store & store, - const std::map, StorePath> & inputDrvOutputs) const + std::function(ref drvPath, const std::string & outputName)> queryResolutionChain) const { BasicDerivation resolved { *this }; @@ -1139,7 +1115,7 @@ std::optional Derivation::tryResolve( for (auto & [inputDrv, inputNode] : inputDrvs.map) if (!tryResolveInput(store, resolved.inputSrcs, inputRewrites, - nullptr, inputDrv, inputNode, inputDrvOutputs)) + nullptr, make_ref(SingleDerivedPath::Opaque{inputDrv}), inputNode, queryResolutionChain)) return std::nullopt; rewriteDerivation(store, resolved, inputRewrites); @@ -1252,13 +1228,13 @@ DerivationOutput DerivationOutput::fromJSON( keys.insert(key); auto methodAlgo = [&]() -> std::pair { - auto & method_ = getString(valueAt(json, "method")); - ContentAddressMethod method = ContentAddressMethod::parse(method_); + ContentAddressMethod method = ContentAddressMethod::parse( + getString(valueAt(json, "method"))); if (method == ContentAddressMethod::Raw::Text) xpSettings.require(Xp::DynamicDerivations); - auto & hashAlgo_ = getString(valueAt(json, "hashAlgo")); - auto hashAlgo = parseHashAlgo(hashAlgo_); + auto hashAlgo = parseHashAlgo( + getString(valueAt(json, "hashAlgo"))); return { std::move(method), std::move(hashAlgo) }; }; @@ -1357,6 +1333,11 @@ nlohmann::json Derivation::toJSON(const StoreDirConfig & store) const res["args"] = args; res["env"] = env; + if (auto it = env.find("__json"); it != env.end()) { + res["env"].erase("__json"); + res["structuredAttrs"] = nlohmann::json::parse(it->second); + } + return res; } @@ -1375,10 +1356,11 @@ Derivation Derivation::fromJSON( res.name = getString(valueAt(json, "name")); try { - for (auto & [outputName, output] : getObject(valueAt(json, "outputs"))) { + auto outputs = getObject(valueAt(json, "outputs")); + for (auto & [outputName, output] : outputs) { res.outputs.insert_or_assign( outputName, - DerivationOutput::fromJSON(store, res.name, outputName, output)); + DerivationOutput::fromJSON(store, res.name, outputName, output, xpSettings)); } } catch (Error & e) { e.addTrace({}, "while reading key 'outputs'"); @@ -1386,7 +1368,8 @@ Derivation Derivation::fromJSON( } try { - for (auto & input : getArray(valueAt(json, "inputSrcs"))) + auto inputSrcs = getArray(valueAt(json, "inputSrcs")); + for (auto & input : inputSrcs) res.inputSrcs.insert(store.parseStorePath(static_cast(input))); } catch (Error & e) { e.addTrace({}, "while reading key 'inputSrcs'"); @@ -1399,13 +1382,15 @@ Derivation Derivation::fromJSON( auto & json = getObject(_json); DerivedPathMap::ChildNode node; node.value = getStringSet(valueAt(json, "outputs")); - for (auto & [outputId, childNode] : getObject(valueAt(json, "dynamicOutputs"))) { + auto drvs = getObject(valueAt(json, "dynamicOutputs")); + for (auto & [outputId, childNode] : drvs) { xpSettings.require(Xp::DynamicDerivations); node.childMap[outputId] = doInput(childNode); } return node; }; - for (auto & [inputDrvPath, inputOutputs] : getObject(valueAt(json, "inputDrvs"))) + auto drvs = getObject(valueAt(json, "inputDrvs")); + for (auto & [inputDrvPath, inputOutputs] : drvs) res.inputDrvs.map[store.parseStorePath(inputDrvPath)] = doInput(inputOutputs); } catch (Error & e) { @@ -1416,7 +1401,17 @@ Derivation Derivation::fromJSON( res.platform = getString(valueAt(json, "system")); res.builder = getString(valueAt(json, "builder")); res.args = getStringList(valueAt(json, "args")); - res.env = getStringMap(valueAt(json, "env")); + + auto envJson = valueAt(json, "env"); + try { + res.env = getStringMap(envJson); + } catch (Error & e) { + e.addTrace({}, "while reading key 'env'"); + throw; + } + + if (auto structuredAttrs = get(json, "structuredAttrs")) + res.env.insert_or_assign("__json", structuredAttrs->dump()); return res; } diff --git a/src/libstore/derived-path-map.cc b/src/libstore/derived-path-map.cc index c97d52773eb..e34deb74497 100644 --- a/src/libstore/derived-path-map.cc +++ b/src/libstore/derived-path-map.cc @@ -1,5 +1,5 @@ -#include "derived-path-map.hh" -#include "util.hh" +#include "nix/store/derived-path-map.hh" +#include "nix/util/util.hh" namespace nix { @@ -52,20 +52,24 @@ typename DerivedPathMap::ChildNode * DerivedPathMap::findSlot(const Single // instantiations +#include "nix/store/build/derivation-trampoline-goal.hh" namespace nix { template<> -bool DerivedPathMap>::ChildNode::operator == ( - const DerivedPathMap>::ChildNode &) const noexcept = default; +bool DerivedPathMap::ChildNode::operator == ( + const DerivedPathMap::ChildNode &) const noexcept = default; // TODO libc++ 16 (used by darwin) missing `std::map::operator <=>`, can't do yet. #if 0 template<> -std::strong_ordering DerivedPathMap>::ChildNode::operator <=> ( - const DerivedPathMap>::ChildNode &) const noexcept = default; +std::strong_ordering DerivedPathMap::ChildNode::operator <=> ( + const DerivedPathMap::ChildNode &) const noexcept = default; #endif -template struct DerivedPathMap>::ChildNode; -template struct DerivedPathMap>; +template struct DerivedPathMap::ChildNode; +template struct DerivedPathMap; + +template struct DerivedPathMap>>; + }; diff --git a/src/libstore/derived-path.cc b/src/libstore/derived-path.cc index 1eef881de0c..6186f05829b 100644 --- a/src/libstore/derived-path.cc +++ b/src/libstore/derived-path.cc @@ -1,7 +1,7 @@ -#include "derived-path.hh" -#include "derivations.hh" -#include "store-api.hh" -#include "comparator.hh" +#include "nix/store/derived-path.hh" +#include "nix/store/derivations.hh" +#include "nix/store/store-api.hh" +#include "nix/util/comparator.hh" #include @@ -170,7 +170,7 @@ void drvRequireExperiment( } SingleDerivedPath::Built SingleDerivedPath::Built::parse( - const StoreDirConfig & store, ref drv, + const StoreDirConfig & store, ref drv, OutputNameView output, const ExperimentalFeatureSettings & xpSettings) { @@ -182,7 +182,7 @@ SingleDerivedPath::Built SingleDerivedPath::Built::parse( } DerivedPath::Built DerivedPath::Built::parse( - const StoreDirConfig & store, ref drv, + const StoreDirConfig & store, ref drv, OutputNameView outputsS, const ExperimentalFeatureSettings & xpSettings) { @@ -201,7 +201,7 @@ static SingleDerivedPath parseWithSingle( return n == s.npos ? (SingleDerivedPath) SingleDerivedPath::Opaque::parse(store, s) : (SingleDerivedPath) SingleDerivedPath::Built::parse(store, - make_ref(parseWithSingle( + make_ref(parseWithSingle( store, s.substr(0, n), separator, @@ -234,7 +234,7 @@ static DerivedPath parseWith( return n == s.npos ? (DerivedPath) DerivedPath::Opaque::parse(store, s) : (DerivedPath) DerivedPath::Built::parse(store, - make_ref(parseWithSingle( + make_ref(parseWithSingle( store, s.substr(0, n), separator, diff --git a/src/libstore/downstream-placeholder.cc b/src/libstore/downstream-placeholder.cc index 91d47f946c1..24ce2ad997a 100644 --- a/src/libstore/downstream-placeholder.cc +++ b/src/libstore/downstream-placeholder.cc @@ -1,5 +1,5 @@ -#include "downstream-placeholder.hh" -#include "derivations.hh" +#include "nix/store/downstream-placeholder.hh" +#include "nix/store/derivations.hh" namespace nix { diff --git a/src/libstore/dummy-store.cc b/src/libstore/dummy-store.cc index c1e871e9384..819c47babce 100644 --- a/src/libstore/dummy-store.cc +++ b/src/libstore/dummy-store.cc @@ -1,9 +1,9 @@ -#include "store-api.hh" -#include "callback.hh" +#include "nix/store/store-registration.hh" +#include "nix/util/callback.hh" namespace nix { -struct DummyStoreConfig : virtual StoreConfig { +struct DummyStoreConfig : public std::enable_shared_from_this, virtual StoreConfig { using StoreConfig::StoreConfig; DummyStoreConfig(std::string_view scheme, std::string_view authority, const Params & params) @@ -13,35 +13,36 @@ struct DummyStoreConfig : virtual StoreConfig { throw UsageError("`%s` store URIs must not contain an authority part %s", scheme, authority); } - const std::string name() override { return "Dummy Store"; } + static const std::string name() { return "Dummy Store"; } - std::string doc() override + static std::string doc() { return #include "dummy-store.md" ; } - static std::set uriSchemes() { + static StringSet uriSchemes() { return {"dummy"}; } + + ref openStore() const override; }; -struct DummyStore : public virtual DummyStoreConfig, public virtual Store +struct DummyStore : virtual Store { - DummyStore(std::string_view scheme, std::string_view authority, const Params & params) - : StoreConfig(params) - , DummyStoreConfig(scheme, authority, params) - , Store(params) - { } + using Config = DummyStoreConfig; - DummyStore(const Params & params) - : DummyStore("dummy", "", params) + ref config; + + DummyStore(ref config) + : Store{*config} + , config(config) { } std::string getUri() override { - return *uriSchemes().begin(); + return *Config::uriSchemes().begin(); } void queryPathInfoUncached(const StorePath & path, @@ -83,9 +84,16 @@ struct DummyStore : public virtual DummyStoreConfig, public virtual Store { callback(nullptr); } virtual ref getFSAccessor(bool requireValidPath) override - { unsupported("getFSAccessor"); } + { + return makeEmptySourceAccessor(); + } }; -static RegisterStoreImplementation regDummyStore; +ref DummyStore::Config::openStore() const +{ + return make_ref(ref{shared_from_this()}); +} + +static RegisterStoreImplementation regDummyStore; } diff --git a/src/libstore/export-import.cc b/src/libstore/export-import.cc index 1c62cdfad64..5bbdd1e5cf5 100644 --- a/src/libstore/export-import.cc +++ b/src/libstore/export-import.cc @@ -1,8 +1,8 @@ -#include "serialise.hh" -#include "store-api.hh" -#include "archive.hh" -#include "common-protocol.hh" -#include "common-protocol-impl.hh" +#include "nix/util/serialise.hh" +#include "nix/store/store-api.hh" +#include "nix/util/archive.hh" +#include "nix/store/common-protocol.hh" +#include "nix/store/common-protocol-impl.hh" #include diff --git a/src/libstore/filetransfer.cc b/src/libstore/filetransfer.cc index 42b93cfe00a..7e29d00e6c0 100644 --- a/src/libstore/filetransfer.cc +++ b/src/libstore/filetransfer.cc @@ -1,19 +1,20 @@ -#include "filetransfer.hh" -#include "globals.hh" -#include "config-global.hh" -#include "store-api.hh" -#include "s3.hh" -#include "compression.hh" -#include "finally.hh" -#include "callback.hh" -#include "signals.hh" - -#if ENABLE_S3 +#include "nix/store/filetransfer.hh" +#include "nix/store/globals.hh" +#include "nix/util/config-global.hh" +#include "nix/store/store-api.hh" +#include "nix/store/s3.hh" +#include "nix/util/compression.hh" +#include "nix/util/finally.hh" +#include "nix/util/callback.hh" +#include "nix/util/signals.hh" + +#include "store-config-private.hh" +#if NIX_WITH_S3_SUPPORT #include #endif -#if __linux__ -# include "namespaces.hh" +#ifdef __linux__ +# include "nix/util/linux-namespaces.hh" #endif #include @@ -21,10 +22,8 @@ #include -#include #include #include -#include #include #include #include @@ -34,6 +33,9 @@ using namespace std::string_literals; namespace nix { +const unsigned int RETRY_TIME_MS_DEFAULT = 250; +const unsigned int RETRY_TIME_MS_TOO_MANY_REQUESTS = 60000; + FileTransferSettings fileTransferSettings; static GlobalConfig::Register rFileTransferSettings(&fileTransferSettings); @@ -94,7 +96,7 @@ struct curlFileTransfer : public FileTransfer : fileTransfer(fileTransfer) , request(request) , act(*logger, lvlTalkative, actFileTransfer, - fmt(request.data ? "uploading '%s'" : "downloading '%s'", request.uri), + fmt("%sing '%s'", request.verb(), request.uri), {request.uri}, request.parentAct) , callback(std::move(callback)) , finalSink([this](std::string_view data) { @@ -261,7 +263,7 @@ struct curlFileTransfer : public FileTransfer return ((TransferItem *) userp)->headerCallback(contents, size, nmemb); } - int progressCallback(double dltotal, double dlnow) + int progressCallback(curl_off_t dltotal, curl_off_t dlnow) { try { act.progress(dlnow, dltotal); @@ -271,9 +273,11 @@ struct curlFileTransfer : public FileTransfer return getInterrupted(); } - static int progressCallbackWrapper(void * userp, double dltotal, double dlnow, double ultotal, double ulnow) + static int progressCallbackWrapper(void * userp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { - return ((TransferItem *) userp)->progressCallback(dltotal, dlnow); + auto & item = *static_cast(userp); + auto isUpload = bool(item.request.data); + return item.progressCallback(isUpload ? ultotal : dltotal, isUpload ? ulnow : dlnow); } static int debugCallback(CURL * handle, curl_infotype type, char * data, size_t size, void * userptr) @@ -300,6 +304,31 @@ struct curlFileTransfer : public FileTransfer return ((TransferItem *) userp)->readCallback(buffer, size, nitems); } + #if !defined(_WIN32) && LIBCURL_VERSION_NUM >= 0x071000 + static int cloexec_callback(void *, curl_socket_t curlfd, curlsocktype purpose) { + unix::closeOnExec(curlfd); + vomit("cloexec set for fd %i", curlfd); + return CURL_SOCKOPT_OK; + } + #endif + + size_t seekCallback(curl_off_t offset, int origin) + { + if (origin == SEEK_SET) { + readOffset = offset; + } else if (origin == SEEK_CUR) { + readOffset += offset; + } else if (origin == SEEK_END) { + readOffset = request.data->length() + offset; + } + return CURL_SEEKFUNC_OK; + } + + static size_t seekCallbackWrapper(void *clientp, curl_off_t offset, int origin) + { + return ((TransferItem *) clientp)->seekCallback(offset, origin); + } + void init() { if (!req) req = curl_easy_init(); @@ -332,8 +361,8 @@ struct curlFileTransfer : public FileTransfer curl_easy_setopt(req, CURLOPT_HEADERFUNCTION, TransferItem::headerCallbackWrapper); curl_easy_setopt(req, CURLOPT_HEADERDATA, this); - curl_easy_setopt(req, CURLOPT_PROGRESSFUNCTION, progressCallbackWrapper); - curl_easy_setopt(req, CURLOPT_PROGRESSDATA, this); + curl_easy_setopt(req, CURLOPT_XFERINFOFUNCTION, progressCallbackWrapper); + curl_easy_setopt(req, CURLOPT_XFERINFODATA, this); curl_easy_setopt(req, CURLOPT_NOPROGRESS, 0); curl_easy_setopt(req, CURLOPT_HTTPHEADER, requestHeaders); @@ -345,10 +374,15 @@ struct curlFileTransfer : public FileTransfer curl_easy_setopt(req, CURLOPT_NOBODY, 1); if (request.data) { - curl_easy_setopt(req, CURLOPT_UPLOAD, 1L); + if (request.post) + curl_easy_setopt(req, CURLOPT_POST, 1L); + else + curl_easy_setopt(req, CURLOPT_UPLOAD, 1L); curl_easy_setopt(req, CURLOPT_READFUNCTION, readCallbackWrapper); curl_easy_setopt(req, CURLOPT_READDATA, this); curl_easy_setopt(req, CURLOPT_INFILESIZE_LARGE, (curl_off_t) request.data->length()); + curl_easy_setopt(req, CURLOPT_SEEKFUNCTION, seekCallbackWrapper); + curl_easy_setopt(req, CURLOPT_SEEKDATA, this); } if (request.verifyTLS) { @@ -359,6 +393,10 @@ struct curlFileTransfer : public FileTransfer curl_easy_setopt(req, CURLOPT_SSL_VERIFYHOST, 0); } + #if !defined(_WIN32) && LIBCURL_VERSION_NUM >= 0x071000 + curl_easy_setopt(req, CURLOPT_SOCKOPTFUNCTION, cloexec_callback); + #endif + curl_easy_setopt(req, CURLOPT_CONNECTTIMEOUT, fileTransferSettings.connectTimeout.get()); curl_easy_setopt(req, CURLOPT_LOW_SPEED_LIMIT, 1L); @@ -383,6 +421,8 @@ struct curlFileTransfer : public FileTransfer { auto finishTime = std::chrono::steady_clock::now(); + auto retryTimeMs = request.baseRetryTimeMs; + auto httpStatus = getHTTPStatus(); debug("finished %s of '%s'; curl status = %d, HTTP status = %d, body = %d bytes, duration = %.2f s", @@ -433,10 +473,12 @@ struct curlFileTransfer : public FileTransfer } else if (httpStatus == 401 || httpStatus == 403 || httpStatus == 407) { // Don't retry on authentication/authorization failures err = Forbidden; - } else if (httpStatus >= 400 && httpStatus < 500 && httpStatus != 408 && httpStatus != 429) { + } else if (httpStatus == 429) { + // 429 means too many requests, so we retry (with a substantially longer delay) + retryTimeMs = RETRY_TIME_MS_TOO_MANY_REQUESTS; + } else if (httpStatus >= 400 && httpStatus < 500 && httpStatus != 408) { // Most 4xx errors are client errors and are probably not worth retrying: // * 408 means the server timed out waiting for us, so we try again - // * 429 means too many requests, so we retry (with a delay) err = Misc; } else if (httpStatus == 501 || httpStatus == 505 || httpStatus == 511) { // Let's treat most 5xx (server) errors as transient, except for a handful: @@ -502,11 +544,13 @@ struct curlFileTransfer : public FileTransfer || writtenToSink == 0 || (acceptRanges && encoding.empty()))) { - int ms = request.baseRetryTimeMs * std::pow(2.0f, attempt - 1 + std::uniform_real_distribution<>(0.0, 0.5)(fileTransfer.mt19937)); + int ms = retryTimeMs * std::pow(2.0f, attempt - 1 + std::uniform_real_distribution<>(0.0, 0.5)(fileTransfer.mt19937)); if (writtenToSink) warn("%s; retrying from offset %d in %d ms", exc.what(), writtenToSink, ms); else warn("%s; retrying in %d ms", exc.what(), ms); + decompressionSink.reset(); + errorSink.reset(); embargo = std::chrono::steady_clock::now() + std::chrono::milliseconds(ms); fileTransfer.enqueueItem(shared_from_this()); } @@ -592,7 +636,7 @@ struct curlFileTransfer : public FileTransfer }); #endif - #if __linux__ + #ifdef __linux__ try { tryUnshareFilesystem(); } catch (nix::Error & e) { @@ -726,8 +770,8 @@ struct curlFileTransfer : public FileTransfer #endif } -#if ENABLE_S3 - std::tuple parseS3Uri(std::string uri) +#if NIX_WITH_S3_SUPPORT + std::tuple parseS3Uri(std::string uri) { auto [path, params] = splitUriAndParams(uri); @@ -749,7 +793,7 @@ struct curlFileTransfer : public FileTransfer if (hasPrefix(request.uri, "s3://")) { // FIXME: do this on a worker thread try { -#if ENABLE_S3 +#if NIX_WITH_S3_SUPPORT auto [bucketName, key, params] = parseS3Uri(request.uri); std::string profile = getOr(params, "profile", ""); @@ -759,15 +803,11 @@ struct curlFileTransfer : public FileTransfer S3Helper s3Helper(profile, region, scheme, endpoint); - Activity act(*logger, lvlTalkative, actFileTransfer, - fmt("downloading '%s'", request.uri), - {request.uri}, request.parentAct); - // FIXME: implement ETag auto s3Res = s3Helper.getObject(bucketName, key); FileTransferResult res; if (!s3Res.data) - throw FileTransferError(NotFound, "S3 object '%s' does not exist", request.uri); + throw FileTransferError(NotFound, {}, "S3 object '%s' does not exist", request.uri); res.data = std::move(*s3Res.data); res.urls.push_back(request.uri); callback(std::move(res)); diff --git a/src/libstore/gc.cc b/src/libstore/gc.cc index 45dfe4ad871..48467381d3f 100644 --- a/src/libstore/gc.cc +++ b/src/libstore/gc.cc @@ -1,20 +1,23 @@ -#include "derivations.hh" -#include "globals.hh" -#include "local-store.hh" -#include "finally.hh" -#include "unix-domain-socket.hh" -#include "signals.hh" -#include "posix-fs-canonicalise.hh" +#include "nix/store/derivations.hh" +#include "nix/store/globals.hh" +#include "nix/store/local-store.hh" +#include "nix/util/finally.hh" +#include "nix/util/unix-domain-socket.hh" +#include "nix/util/signals.hh" +#include "nix/store/posix-fs-canonicalise.hh" + +#include "store-config-private.hh" #if !defined(__linux__) // For shelling out to lsof -# include "processes.hh" +# include "nix/util/processes.hh" #endif +#include + #include #include #include -#include #include #include @@ -41,7 +44,7 @@ static std::string gcRootsDir = "gcroots"; void LocalStore::addIndirectRoot(const Path & path) { std::string hash = hashString(HashAlgorithm::SHA1, path).to_string(HashFormat::Nix32, false); - Path realRoot = canonPath(fmt("%1%/%2%/auto/%3%", stateDir, gcRootsDir, hash)); + Path realRoot = canonPath(fmt("%1%/%2%/auto/%3%", config->stateDir, gcRootsDir, hash)); makeSymlink(realRoot, path); } @@ -80,7 +83,7 @@ void LocalStore::createTempRootsFile() void LocalStore::addTempRoot(const StorePath & path) { - if (readOnly) { + if (config->readOnly) { debug("Read-only store doesn't support creating lock files for temp roots, but nothing can be deleted anyways."); return; } @@ -107,7 +110,7 @@ void LocalStore::addTempRoot(const StorePath & path) auto fdRootsSocket(_fdRootsSocket.lock()); if (!*fdRootsSocket) { - auto socketPath = stateDir.get() + gcSocketPath; + auto socketPath = config->stateDir.get() + gcSocketPath; debug("connecting to '%s'", socketPath); *fdRootsSocket = createUnixDomainSocket(); try { @@ -162,7 +165,7 @@ void LocalStore::findTempRoots(Roots & tempRoots, bool censor) { /* Read the `temproots' directory for per-process temporary root files. */ - for (auto & i : std::filesystem::directory_iterator{tempRootsDir}) { + for (auto & i : DirectoryIterator{tempRootsDir}) { checkInterrupt(); auto name = i.path().filename().string(); if (name[0] == '.') { @@ -230,7 +233,7 @@ void LocalStore::findRoots(const Path & path, std::filesystem::file_type type, R type = std::filesystem::symlink_status(path).type(); if (type == std::filesystem::file_type::directory) { - for (auto & i : std::filesystem::directory_iterator{path}) { + for (auto & i : DirectoryIterator{path}) { checkInterrupt(); findRoots(i.path().string(), i.symlink_status().type(), roots); } @@ -245,7 +248,7 @@ void LocalStore::findRoots(const Path & path, std::filesystem::file_type type, R else { target = absPath(target, dirOf(path)); if (!pathExists(target)) { - if (isInDir(path, stateDir + "/" + gcRootsDir + "/auto")) { + if (isInDir(path, std::filesystem::path{config->stateDir.get()} / gcRootsDir / "auto")) { printInfo("removing stale link from '%1%' to '%2%'", path, target); unlink(path.c_str()); } @@ -286,8 +289,8 @@ void LocalStore::findRoots(const Path & path, std::filesystem::file_type type, R void LocalStore::findRootsNoTemp(Roots & roots, bool censor) { /* Process direct roots in {gcroots,profiles}. */ - findRoots(stateDir + "/" + gcRootsDir, std::filesystem::file_type::unknown, roots); - findRoots(stateDir + "/profiles", std::filesystem::file_type::unknown, roots); + findRoots(config->stateDir + "/" + gcRootsDir, std::filesystem::file_type::unknown, roots); + findRoots(config->stateDir + "/profiles", std::filesystem::file_type::unknown, roots); /* Add additional roots returned by different platforms-specific heuristics. This is typically used to add running programs to @@ -329,11 +332,11 @@ static void readProcLink(const std::filesystem::path & file, UncheckedRoots & ro static std::string quoteRegexChars(const std::string & raw) { - static auto specialRegex = std::regex(R"([.^$\\*+?()\[\]{}|])"); - return std::regex_replace(raw, specialRegex, R"(\$&)"); + static auto specialRegex = boost::regex(R"([.^$\\*+?()\[\]{}|])"); + return boost::regex_replace(raw, specialRegex, R"(\$&)"); } -#if __linux__ +#ifdef __linux__ static void readFileRoots(const std::filesystem::path & path, UncheckedRoots & roots) { try { @@ -352,12 +355,12 @@ void LocalStore::findRuntimeRoots(Roots & roots, bool censor) auto procDir = AutoCloseDir{opendir("/proc")}; if (procDir) { struct dirent * ent; - auto digitsRegex = std::regex(R"(^\d+$)"); - auto mapRegex = std::regex(R"(^\s*\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(/\S+)\s*$)"); - auto storePathRegex = std::regex(quoteRegexChars(storeDir) + R"(/[0-9a-z]+[0-9a-zA-Z\+\-\._\?=]*)"); + static const auto digitsRegex = boost::regex(R"(^\d+$)"); + static const auto mapRegex = boost::regex(R"(^\s*\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(/\S+)\s*$)"); + auto storePathRegex = boost::regex(quoteRegexChars(storeDir) + R"(/[0-9a-z]+[0-9a-zA-Z\+\-\._\?=]*)"); while (errno = 0, ent = readdir(procDir.get())) { checkInterrupt(); - if (std::regex_match(ent->d_name, digitsRegex)) { + if (boost::regex_match(ent->d_name, digitsRegex)) { try { readProcLink(fmt("/proc/%s/exe" ,ent->d_name), unchecked); readProcLink(fmt("/proc/%s/cwd", ent->d_name), unchecked); @@ -384,15 +387,15 @@ void LocalStore::findRuntimeRoots(Roots & roots, bool censor) std::filesystem::path mapFile = fmt("/proc/%s/maps", ent->d_name); auto mapLines = tokenizeString>(readFile(mapFile.string()), "\n"); for (const auto & line : mapLines) { - auto match = std::smatch{}; - if (std::regex_match(line, match, mapRegex)) + auto match = boost::smatch{}; + if (boost::regex_match(line, match, mapRegex)) unchecked[match[1]].emplace(mapFile.string()); } auto envFile = fmt("/proc/%s/environ", ent->d_name); auto envString = readFile(envFile); - auto env_end = std::sregex_iterator{}; - for (auto i = std::sregex_iterator{envString.begin(), envString.end(), storePathRegex}; i != env_end; ++i) + auto env_end = boost::sregex_iterator{}; + for (auto i = boost::sregex_iterator{envString.begin(), envString.end(), storePathRegex}; i != env_end; ++i) unchecked[i->str()].emplace(envFile); } catch (SystemError & e) { if (errno == ENOENT || errno == EACCES || errno == ESRCH) @@ -411,12 +414,12 @@ void LocalStore::findRuntimeRoots(Roots & roots, bool censor) // Because of this we disable lsof when running the tests. if (getEnv("_NIX_TEST_NO_LSOF") != "1") { try { - std::regex lsofRegex(R"(^n(/.*)$)"); + boost::regex lsofRegex(R"(^n(/.*)$)"); auto lsofLines = tokenizeString>(runProgram(LSOF, true, { "-n", "-w", "-F", "n" }), "\n"); for (const auto & line : lsofLines) { - std::smatch match; - if (std::regex_match(line, match, lsofRegex)) + boost::smatch match; + if (boost::regex_match(line, match, lsofRegex)) unchecked[match[1].str()].emplace("{lsof}"); } } catch (ExecError & e) { @@ -425,7 +428,7 @@ void LocalStore::findRuntimeRoots(Roots & roots, bool censor) } #endif -#if __linux__ +#ifdef __linux__ readFileRoots("/proc/sys/kernel/modprobe", unchecked); readFileRoots("/proc/sys/kernel/fbsplash", unchecked); readFileRoots("/proc/sys/kernel/poweroff_cmd", unchecked); @@ -455,7 +458,7 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) bool gcKeepOutputs = settings.gcKeepOutputs; bool gcKeepDerivations = settings.gcKeepDerivations; - StorePathSet roots, dead, alive; + std::unordered_set roots, dead, alive; struct Shared { @@ -496,7 +499,7 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) readFile(*p); /* Start the server for receiving new roots. */ - auto socketPath = stateDir.get() + gcSocketPath; + auto socketPath = config->stateDir.get() + gcSocketPath; createDirs(dirOf(socketPath)); auto fdServer = createUnixDomainSocket(socketPath, 0666); @@ -633,7 +636,7 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) auto deleteFromStore = [&](std::string_view baseName) { Path path = storeDir + "/" + std::string(baseName); - Path realPath = realStoreDir + "/" + std::string(baseName); + Path realPath = config->realStoreDir + "/" + std::string(baseName); /* There may be temp directories in the store that are still in use by another process. We need to be sure that we can acquire an @@ -661,7 +664,7 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) } }; - std::map referrersCache; + std::unordered_map referrersCache; /* Helper function that visits all paths reachable from `start` via the referrers edges and optionally derivers and derivation @@ -772,7 +775,7 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) deleteFromStore(path.to_string()); referrersCache.erase(path); } catch (PathInUse &e) { - // If we end up here, it's likely a new occurence + // If we end up here, it's likely a new occurrence // of https://github.com/NixOS/nix/issues/11923 printError("BUG: %s", e.what()); } @@ -802,8 +805,8 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) printInfo("determining live/dead paths..."); try { - AutoCloseDir dir(opendir(realStoreDir.get().c_str())); - if (!dir) throw SysError("opening directory '%1%'", realStoreDir); + AutoCloseDir dir(opendir(config->realStoreDir.get().c_str())); + if (!dir) throw SysError("opening directory '%1%'", config->realStoreDir); /* Read the store and delete all paths that are invalid or unreachable. We don't use readDirectory() here so that @@ -905,8 +908,8 @@ void LocalStore::autoGC(bool sync) return std::stoll(readFile(*fakeFreeSpaceFile)); struct statvfs st; - if (statvfs(realStoreDir.get().c_str(), &st)) - throw SysError("getting filesystem info about '%s'", realStoreDir); + if (statvfs(config->realStoreDir.get().c_str(), &st)) + throw SysError("getting filesystem info about '%s'", config->realStoreDir); return (uint64_t) st.f_bavail * st.f_frsize; }; diff --git a/src/libstore/globals.cc b/src/libstore/globals.cc index b64e73c265b..1f80cb379e5 100644 --- a/src/libstore/globals.cc +++ b/src/libstore/globals.cc @@ -1,11 +1,11 @@ -#include "globals.hh" -#include "config-global.hh" -#include "current-process.hh" -#include "archive.hh" -#include "args.hh" -#include "abstract-setting-to-json.hh" -#include "compute-levels.hh" -#include "signals.hh" +#include "nix/store/globals.hh" +#include "nix/util/config-global.hh" +#include "nix/util/current-process.hh" +#include "nix/util/archive.hh" +#include "nix/util/args.hh" +#include "nix/util/abstract-setting-to-json.hh" +#include "nix/util/compute-levels.hh" +#include "nix/util/signals.hh" #include #include @@ -25,17 +25,17 @@ # include #endif -#if __APPLE__ -# include "processes.hh" +#ifdef __APPLE__ +# include "nix/util/processes.hh" #endif -#include "config-impl.hh" +#include "nix/util/config-impl.hh" #ifdef __APPLE__ #include #endif -#include "strings.hh" +#include "store-config-private.hh" namespace nix { @@ -65,7 +65,6 @@ Settings::Settings() , nixStateDir(canonPath(getEnvNonEmpty("NIX_STATE_DIR").value_or(NIX_STATE_DIR))) , nixConfDir(canonPath(getEnvNonEmpty("NIX_CONF_DIR").value_or(NIX_CONF_DIR))) , nixUserConfFiles(getUserConfigFiles()) - , nixManDir(canonPath(NIX_MAN_DIR)) , nixDaemonSocketFile(canonPath(getEnvNonEmpty("NIX_DAEMON_SOCKET_PATH").value_or(nixStateDir + DEFAULT_SOCKET_PATH))) { #ifndef _WIN32 @@ -86,12 +85,12 @@ Settings::Settings() builders = concatStringsSep("\n", ss); } -#if defined(__linux__) && defined(SANDBOX_SHELL) +#if (defined(__linux__) || defined(__FreeBSD__)) && defined(SANDBOX_SHELL) sandboxPaths = tokenizeString("/bin/sh=" SANDBOX_SHELL); #endif /* chroot-like behavior from Apple's sandbox */ -#if __APPLE__ +#ifdef __APPLE__ sandboxPaths = tokenizeString("/System/Library/Frameworks /System/Library/PrivateFrameworks /bin/sh /bin/bash /private/tmp /private/var/tmp /usr/lib"); allowedImpureHostPrefixes = tokenizeString("/System/Library /usr/lib /dev /bin/sh"); #endif @@ -141,7 +140,7 @@ std::vector getUserConfigFiles() return files; } -unsigned int Settings::getDefaultCores() +unsigned int Settings::getDefaultCores() const { const unsigned int concurrency = std::max(1U, std::thread::hardware_concurrency()); const unsigned int maxCPU = getMaxCPU(); @@ -152,7 +151,7 @@ unsigned int Settings::getDefaultCores() return concurrency; } -#if __APPLE__ +#ifdef __APPLE__ static bool hasVirt() { int hasVMM; @@ -182,16 +181,16 @@ StringSet Settings::getDefaultSystemFeatures() actually require anything special on the machines. */ StringSet features{"nixos-test", "benchmark", "big-parallel"}; - #if __linux__ + #ifdef __linux__ features.insert("uid-range"); #endif - #if __linux__ + #ifdef __linux__ if (access("/dev/kvm", R_OK | W_OK) == 0) features.insert("kvm"); #endif - #if __APPLE__ + #ifdef __APPLE__ if (hasVirt()) features.insert("apple-virt"); #endif @@ -203,19 +202,19 @@ StringSet Settings::getDefaultExtraPlatforms() { StringSet extraPlatforms; - if (std::string{SYSTEM} == "x86_64-linux" && !isWSL1()) + if (std::string{NIX_LOCAL_SYSTEM} == "x86_64-linux" && !isWSL1()) extraPlatforms.insert("i686-linux"); -#if __linux__ +#ifdef __linux__ StringSet levels = computeLevels(); for (auto iter = levels.begin(); iter != levels.end(); ++iter) extraPlatforms.insert(*iter + "-linux"); -#elif __APPLE__ +#elif defined(__APPLE__) // Rosetta 2 emulation layer can run x86_64 binaries on aarch64 // machines. Note that we can’t force processes from executing // x86_64 in aarch64 environments or vice versa since they can // always exec with their own binary preferences. - if (std::string{SYSTEM} == "aarch64-darwin" && + if (std::string{NIX_LOCAL_SYSTEM} == "aarch64-darwin" && runProgram(RunOptions {.program = "arch", .args = {"-arch", "x86_64", "/usr/bin/true"}, .mergeStderrToStdout = true}).first == 0) extraPlatforms.insert("x86_64-darwin"); #endif @@ -225,7 +224,7 @@ StringSet Settings::getDefaultExtraPlatforms() bool Settings::isWSL1() { -#if __linux__ +#ifdef __linux__ struct utsname utsbuf; uname(&utsbuf); // WSL1 uses -Microsoft suffix @@ -243,7 +242,7 @@ Path Settings::getDefaultSSLCertFile() return ""; } -const std::string nixVersion = PACKAGE_VERSION; +std::string nixVersion = PACKAGE_VERSION; NLOHMANN_JSON_SERIALIZE_ENUM(SandboxMode, { {SandboxMode::smEnabled, true}, @@ -279,21 +278,21 @@ template<> void BaseSetting::convertToArg(Args & args, const std::s .aliases = aliases, .description = "Enable sandboxing.", .category = category, - .handler = {[this]() { override(smEnabled); }} + .handler = {[this]() { override(smEnabled); }}, }); args.addFlag({ .longName = "no-" + name, .aliases = aliases, .description = "Disable sandboxing.", .category = category, - .handler = {[this]() { override(smDisabled); }} + .handler = {[this]() { override(smDisabled); }}, }); args.addFlag({ .longName = "relaxed-" + name, .aliases = aliases, .description = "Enable sandboxing, but allow builds to disable it.", .category = category, - .handler = {[this]() { override(smRelaxed); }} + .handler = {[this]() { override(smRelaxed); }}, }); } @@ -375,7 +374,7 @@ void initLibStore(bool loadConfig) { [1] https://github.com/apple-oss-distributions/objc4/blob/01edf1705fbc3ff78a423cd21e03dfc21eb4d780/runtime/objc-initialize.mm#L614-L636 */ curl_global_init(CURL_GLOBAL_ALL); -#if __APPLE__ +#ifdef __APPLE__ /* On macOS, don't use the per-session TMPDIR (as set e.g. by sshd). This breaks build users because they don't have access to the TMPDIR, in particular in ‘nix-store --serve’. */ diff --git a/src/libstore/http-binary-cache-store.cc b/src/libstore/http-binary-cache-store.cc index fc7ac2deac8..e44d146b9ee 100644 --- a/src/libstore/http-binary-cache-store.cc +++ b/src/libstore/http-binary-cache-store.cc @@ -1,14 +1,24 @@ -#include "http-binary-cache-store.hh" -#include "filetransfer.hh" -#include "globals.hh" -#include "nar-info-disk-cache.hh" -#include "callback.hh" +#include "nix/store/http-binary-cache-store.hh" +#include "nix/store/filetransfer.hh" +#include "nix/store/globals.hh" +#include "nix/store/nar-info-disk-cache.hh" +#include "nix/util/callback.hh" +#include "nix/store/store-registration.hh" namespace nix { MakeError(UploadToHTTP, Error); +StringSet HttpBinaryCacheStoreConfig::uriSchemes() +{ + static bool forceHttp = getEnv("_NIX_FORCE_HTTP") == "1"; + auto ret = StringSet{"http", "https"}; + if (forceHttp) + ret.insert("file"); + return ret; +} + HttpBinaryCacheStoreConfig::HttpBinaryCacheStoreConfig( std::string_view scheme, std::string_view _cacheUri, @@ -35,10 +45,9 @@ std::string HttpBinaryCacheStoreConfig::doc() } -class HttpBinaryCacheStore : public virtual HttpBinaryCacheStoreConfig, public virtual BinaryCacheStore +class HttpBinaryCacheStore : + public virtual BinaryCacheStore { -private: - struct State { bool enabled = true; @@ -49,37 +58,37 @@ class HttpBinaryCacheStore : public virtual HttpBinaryCacheStoreConfig, public v public: - HttpBinaryCacheStore( - std::string_view scheme, - PathView cacheUri, - const Params & params) - : StoreConfig(params) - , BinaryCacheStoreConfig(params) - , HttpBinaryCacheStoreConfig(scheme, cacheUri, params) - , Store(params) - , BinaryCacheStore(params) + using Config = HttpBinaryCacheStoreConfig; + + ref config; + + HttpBinaryCacheStore(ref config) + : Store{*config} + // TODO it will actually mutate the configuration + , BinaryCacheStore{*config} + , config{config} { diskCache = getNarInfoDiskCache(); } std::string getUri() override { - return cacheUri; + return config->cacheUri; } void init() override { // FIXME: do this lazily? - if (auto cacheInfo = diskCache->upToDateCacheExists(cacheUri)) { - wantMassQuery.setDefault(cacheInfo->wantMassQuery); - priority.setDefault(cacheInfo->priority); + if (auto cacheInfo = diskCache->upToDateCacheExists(config->cacheUri)) { + config->wantMassQuery.setDefault(cacheInfo->wantMassQuery); + config->priority.setDefault(cacheInfo->priority); } else { try { BinaryCacheStore::init(); } catch (UploadToHTTP &) { - throw Error("'%s' does not appear to be a binary cache", cacheUri); + throw Error("'%s' does not appear to be a binary cache", config->cacheUri); } - diskCache->createCache(cacheUri, storeDir, wantMassQuery, priority); + diskCache->createCache(config->cacheUri, config->storeDir, config->wantMassQuery, config->priority); } } @@ -137,7 +146,7 @@ class HttpBinaryCacheStore : public virtual HttpBinaryCacheStoreConfig, public v try { getFileTransfer()->upload(req); } catch (FileTransferError & e) { - throw UploadToHTTP("while uploading to HTTP binary cache at '%s': %s", cacheUri, e.msg()); + throw UploadToHTTP("while uploading to HTTP binary cache at '%s': %s", config->cacheUri, e.msg()); } } @@ -146,7 +155,7 @@ class HttpBinaryCacheStore : public virtual HttpBinaryCacheStoreConfig, public v return FileTransferRequest( hasPrefix(path, "https://") || hasPrefix(path, "http://") || hasPrefix(path, "file://") ? path - : cacheUri + "/" + path); + : config->cacheUri + "/" + path); } @@ -167,13 +176,13 @@ class HttpBinaryCacheStore : public virtual HttpBinaryCacheStoreConfig, public v void getFile(const std::string & path, Callback> callback) noexcept override { + auto callbackPtr = std::make_shared(std::move(callback)); + try { checkEnabled(); auto request(makeRequest(path)); - auto callbackPtr = std::make_shared(std::move(callback)); - getFileTransfer()->enqueueFileTransfer(request, {[callbackPtr, this](std::future result) { try { @@ -189,11 +198,24 @@ class HttpBinaryCacheStore : public virtual HttpBinaryCacheStoreConfig, public v }}); } catch (...) { - callback.rethrow(); + callbackPtr->rethrow(); return; } } + std::optional getNixCacheInfo() override + { + try { + auto result = getFileTransfer()->download(makeRequest(cacheInfoFile)); + return result.data; + } catch (FileTransferError & e) { + if (e.error == FileTransfer::NotFound) + return std::nullopt; + maybeDisable(); + throw; + } + } + /** * This isn't actually necessary read only. We support "upsert" now, so we * have a notion of authentication via HTTP POST/PUT. @@ -208,6 +230,14 @@ class HttpBinaryCacheStore : public virtual HttpBinaryCacheStoreConfig, public v } }; -static RegisterStoreImplementation regHttpBinaryCacheStore; +ref HttpBinaryCacheStore::Config::openStore() const +{ + return make_ref(ref{ + // FIXME we shouldn't actually need a mutable config + std::const_pointer_cast(shared_from_this()) + }); +} + +static RegisterStoreImplementation regHttpBinaryCacheStore; } diff --git a/src/libstore/http-binary-cache-store.hh b/src/libstore/http-binary-cache-store.hh deleted file mode 100644 index d2fc43210a2..00000000000 --- a/src/libstore/http-binary-cache-store.hh +++ /dev/null @@ -1,30 +0,0 @@ -#include "binary-cache-store.hh" - -namespace nix { - -struct HttpBinaryCacheStoreConfig : virtual BinaryCacheStoreConfig -{ - using BinaryCacheStoreConfig::BinaryCacheStoreConfig; - - HttpBinaryCacheStoreConfig(std::string_view scheme, std::string_view _cacheUri, const Params & params); - - Path cacheUri; - - const std::string name() override - { - return "HTTP Binary Cache Store"; - } - - static std::set uriSchemes() - { - static bool forceHttp = getEnv("_NIX_FORCE_HTTP") == "1"; - auto ret = std::set({"http", "https"}); - if (forceHttp) - ret.insert("file"); - return ret; - } - - std::string doc() override; -}; - -} diff --git a/src/libstore/binary-cache-store.hh b/src/libstore/include/nix/store/binary-cache-store.hh similarity index 84% rename from src/libstore/binary-cache-store.hh rename to src/libstore/include/nix/store/binary-cache-store.hh index 695bc925277..43f2cf690dc 100644 --- a/src/libstore/binary-cache-store.hh +++ b/src/libstore/include/nix/store/binary-cache-store.hh @@ -1,11 +1,11 @@ #pragma once ///@file -#include "signature/local-keys.hh" -#include "store-api.hh" -#include "log-store.hh" +#include "nix/util/signature/local-keys.hh" +#include "nix/store/store-api.hh" +#include "nix/store/log-store.hh" -#include "pool.hh" +#include "nix/util/pool.hh" #include @@ -32,6 +32,9 @@ struct BinaryCacheStoreConfig : virtual StoreConfig const Setting secretKeyFile{this, "", "secret-key", "Path to the secret key used to sign the binary cache."}; + const Setting secretKeyFiles{this, "", "secret-keys", + "List of comma-separated paths to the secret keys used to sign the binary cache."}; + const Setting localNarCache{this, "", "local-nar-cache", "Path to a local cache of NARs fetched from this binary cache, used by commands such as `nix store cat`."}; @@ -51,20 +54,29 @@ struct BinaryCacheStoreConfig : virtual StoreConfig * @note subclasses must implement at least one of the two * virtual getFile() methods. */ -class BinaryCacheStore : public virtual BinaryCacheStoreConfig, - public virtual Store, - public virtual LogStore +struct BinaryCacheStore : + virtual Store, + virtual LogStore { + using Config = BinaryCacheStoreConfig; + + /** + * Intentionally mutable because some things we update due to the + * cache's own (remote side) settings. + */ + Config & config; private: - std::unique_ptr signer; + std::vector> signers; protected: // The prefix under which realisation infos will be stored const std::string realisationsPrefix = "realisations"; - BinaryCacheStore(const Params & params); + const std::string cacheInfoFile = "nix-cache-info"; + + BinaryCacheStore(Config &); public: @@ -84,6 +96,12 @@ public: */ virtual void getFile(const std::string & path, Sink & sink); + /** + * Get the contents of /nix-cache-info. Return std::nullopt if it + * doesn't exist. + */ + virtual std::optional getNixCacheInfo(); + /** * Fetch the specified file and call the specified callback with * the result. A subclass may implement this asynchronously. diff --git a/src/libstore/build-result.hh b/src/libstore/include/nix/store/build-result.hh similarity index 96% rename from src/libstore/build-result.hh rename to src/libstore/include/nix/store/build-result.hh index 8c66cfeb353..088b057b65c 100644 --- a/src/libstore/build-result.hh +++ b/src/libstore/include/nix/store/build-result.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "realisation.hh" -#include "derived-path.hh" +#include "nix/store/realisation.hh" +#include "nix/store/derived-path.hh" #include #include @@ -14,7 +14,7 @@ struct BuildResult { /** * @note This is directly used in the nix-store --serve protocol. - * That means we need to worry about compatability across versions. + * That means we need to worry about compatibility across versions. * Therefore, don't remove status codes, and only add new status * codes at the end of the list. */ diff --git a/src/libstore/include/nix/store/build/derivation-building-goal.hh b/src/libstore/include/nix/store/build/derivation-building-goal.hh new file mode 100644 index 00000000000..569d1ddbb78 --- /dev/null +++ b/src/libstore/include/nix/store/build/derivation-building-goal.hh @@ -0,0 +1,196 @@ +#pragma once +///@file + +#include "nix/store/parsed-derivations.hh" +#include "nix/store/derivations.hh" +#include "nix/store/derivation-options.hh" +#include "nix/store/build/derivation-building-misc.hh" +#include "nix/store/outputs-spec.hh" +#include "nix/store/store-api.hh" +#include "nix/store/pathlocks.hh" +#include "nix/store/build/goal.hh" + +namespace nix { + +using std::map; + +#ifndef _WIN32 // TODO enable build hook on Windows +struct HookInstance; +struct DerivationBuilder; +#endif + +typedef enum {rpAccept, rpDecline, rpPostpone} HookReply; + +/** Used internally */ +void runPostBuildHook( + Store & store, + Logger & logger, + const StorePath & drvPath, + const StorePathSet & outputPaths); + +/** + * A goal for building a derivation. Substitution, (or any other method of + * obtaining the outputs) will not be attempted, so it is the calling goal's + * responsibility to try to substitute first. + */ +struct DerivationBuildingGoal : public Goal +{ + /** The path of the derivation. */ + StorePath drvPath; + + /** + * The derivation stored at drvPath. + */ + std::unique_ptr drv; + + std::unique_ptr parsedDrv; + std::unique_ptr drvOptions; + + /** + * The remainder is state held during the build. + */ + + /** + * Locks on (fixed) output paths. + */ + PathLocks outputLocks; + + /** + * All input paths (that is, the union of FS closures of the + * immediate input paths). + */ + StorePathSet inputPaths; + + std::map initialOutputs; + + /** + * File descriptor for the log file. + */ + AutoCloseFD fdLogFile; + std::shared_ptr logFileSink, logSink; + + /** + * Number of bytes received from the builder's stdout/stderr. + */ + unsigned long logSize; + + /** + * The most recent log lines. + */ + std::list logTail; + + std::string currentLogLine; + size_t currentLogLinePos = 0; // to handle carriage return + + std::string currentHookLine; + +#ifndef _WIN32 // TODO enable build hook on Windows + /** + * The build hook. + */ + std::unique_ptr hook; + + std::unique_ptr builder; +#endif + + BuildMode buildMode; + + std::unique_ptr> mcRunningBuilds; + + std::unique_ptr act; + + /** + * Activity that denotes waiting for a lock. + */ + std::unique_ptr actLock; + + std::map builderActivities; + + /** + * The remote machine on which we're building. + */ + std::string machineName; + + DerivationBuildingGoal(const StorePath & drvPath, const Derivation & drv, + Worker & worker, + BuildMode buildMode = bmNormal); + ~DerivationBuildingGoal(); + + void timedOut(Error && ex) override; + + std::string key() override; + + /** + * The states. + */ + Co gaveUpOnSubstitution(); + Co tryToBuild(); + Co hookDone(); + + /** + * Is the build hook willing to perform the build? + */ + HookReply tryBuildHook(); + + /** + * Open a log file and a pipe to it. + */ + Path openLogFile(); + + /** + * Close the log file. + */ + void closeLogFile(); + + bool isReadDesc(Descriptor fd); + + /** + * Callback used by the worker to write to the log. + */ + void handleChildOutput(Descriptor fd, std::string_view data) override; + void handleEOF(Descriptor fd) override; + void flushLine(); + + /** + * Wrappers around the corresponding Store methods that first consult the + * derivation. This is currently needed because when there is no drv file + * there also is no DB entry. + */ + std::map> queryPartialDerivationOutputMap(); + + /** + * Update 'initialOutputs' to determine the current status of the + * outputs of the derivation. Also returns a Boolean denoting + * whether all outputs are valid and non-corrupt, and a + * 'SingleDrvOutputs' structure containing the valid outputs. + */ + std::pair checkPathValidity(); + + /** + * Aborts if any output is not valid or corrupt, and otherwise + * returns a 'SingleDrvOutputs' structure containing all outputs. + */ + SingleDrvOutputs assertPathValidity(); + + /** + * Forcibly kill the child process, if any. + */ + void killChild(); + + void started(); + + Done done( + BuildResult::Status status, + SingleDrvOutputs builtOutputs = {}, + std::optional ex = {}); + + void appendLogTailErrorMsg(std::string & msg); + + StorePathSet exportReferences(const StorePathSet & storePaths); + + JobCategory jobCategory() const override { + return JobCategory::Build; + }; +}; + +} diff --git a/src/libstore/include/nix/store/build/derivation-building-misc.hh b/src/libstore/include/nix/store/build/derivation-building-misc.hh new file mode 100644 index 00000000000..3259c5e366d --- /dev/null +++ b/src/libstore/include/nix/store/build/derivation-building-misc.hh @@ -0,0 +1,58 @@ +#pragma once +/** + * @file Misc type definitions for both local building and remote (RPC building) + */ + +#include "nix/util/hash.hh" +#include "nix/store/path.hh" + +namespace nix { + +class Store; +struct Derivation; + +/** + * Unless we are repairing, we don't both to test validity and just assume it, + * so the choices are `Absent` or `Valid`. + */ +enum struct PathStatus { + Corrupt, + Absent, + Valid, +}; + +struct InitialOutputStatus +{ + StorePath path; + PathStatus status; + /** + * Valid in the store, and additionally non-corrupt if we are repairing + */ + bool isValid() const + { + return status == PathStatus::Valid; + } + /** + * Merely present, allowed to be corrupt + */ + bool isPresent() const + { + return status == PathStatus::Corrupt || status == PathStatus::Valid; + } +}; + +struct InitialOutput +{ + bool wanted; + Hash outputHash; + std::optional known; +}; + +void runPostBuildHook(Store & store, Logger & logger, const StorePath & drvPath, const StorePathSet & outputPaths); + +/** + * Format the known outputs of a derivation for use in error messages. + */ +std::string showKnownOutputs(Store & store, const Derivation & drv); + +} diff --git a/src/libstore/include/nix/store/build/derivation-goal.hh b/src/libstore/include/nix/store/build/derivation-goal.hh new file mode 100644 index 00000000000..ac9ec534683 --- /dev/null +++ b/src/libstore/include/nix/store/build/derivation-goal.hh @@ -0,0 +1,113 @@ +#pragma once +///@file + +#include "nix/store/parsed-derivations.hh" +#include "nix/store/derivations.hh" +#include "nix/store/derivation-options.hh" +#include "nix/store/build/derivation-building-misc.hh" +#include "nix/store/outputs-spec.hh" +#include "nix/store/store-api.hh" +#include "nix/store/pathlocks.hh" +#include "nix/store/build/goal.hh" + +namespace nix { + +using std::map; + +/** Used internally */ +void runPostBuildHook( + Store & store, + Logger & logger, + const StorePath & drvPath, + const StorePathSet & outputPaths); + +/** + * A goal for realising a single output of a derivation. Various sorts of + * fetching (which will be done by other goal types) is tried, and if none of + * those succeed, the derivation is attempted to be built. + * + * This is a purely "administrative" goal type, which doesn't do any + * "real work" of substituting (that would be `PathSubstitutionGoal` or + * `DrvOutputSubstitutionGoal`) or building (that would be a + * `DerivationBuildingGoal`). This goal type creates those types of + * goals to attempt each way of realisation a derivation; they are tried + * sequentially in order of preference. + * + * The derivation must already be gotten (in memory, in C++, parsed) and passed + * to the caller. If the derivation itself needs to be gotten first, a + * `DerivationTrampolineGoal` goal must be used instead. + */ +struct DerivationGoal : public Goal +{ + /** The path of the derivation. */ + StorePath drvPath; + + /** + * The specific outputs that we need to build. + */ + OutputName wantedOutput; + + /** + * The derivation stored at drvPath. + */ + std::unique_ptr drv; + + /** + * The remainder is state held during the build. + */ + + std::map initialOutputs; + + BuildMode buildMode; + + std::unique_ptr> mcExpectedBuilds; + + DerivationGoal(const StorePath & drvPath, const Derivation & drv, + const OutputName & wantedOutput, Worker & worker, + BuildMode buildMode = bmNormal); + ~DerivationGoal() = default; + + void timedOut(Error && ex) override { unreachable(); }; + + std::string key() override; + + /** + * The states. + */ + Co haveDerivation(); + + /** + * Wrappers around the corresponding Store methods that first consult the + * derivation. This is currently needed because when there is no drv file + * there also is no DB entry. + */ + std::map> queryPartialDerivationOutputMap(); + OutputPathMap queryDerivationOutputMap(); + + /** + * Update 'initialOutputs' to determine the current status of the + * outputs of the derivation. Also returns a Boolean denoting + * whether all outputs are valid and non-corrupt, and a + * 'SingleDrvOutputs' structure containing the valid outputs. + */ + std::pair checkPathValidity(); + + /** + * Aborts if any output is not valid or corrupt, and otherwise + * returns a 'SingleDrvOutputs' structure containing all outputs. + */ + SingleDrvOutputs assertPathValidity(); + + Co repairClosure(); + + Done done( + BuildResult::Status status, + SingleDrvOutputs builtOutputs = {}, + std::optional ex = {}); + + JobCategory jobCategory() const override { + return JobCategory::Administration; + }; +}; + +} diff --git a/src/libstore/include/nix/store/build/derivation-trampoline-goal.hh b/src/libstore/include/nix/store/build/derivation-trampoline-goal.hh new file mode 100644 index 00000000000..6483376957f --- /dev/null +++ b/src/libstore/include/nix/store/build/derivation-trampoline-goal.hh @@ -0,0 +1,134 @@ +#pragma once +///@file + +#include "nix/store/parsed-derivations.hh" +#include "nix/store/store-api.hh" +#include "nix/store/pathlocks.hh" +#include "nix/store/build/goal.hh" + +namespace nix { + +/** + * This is the "outermost" goal type relating to derivations --- by that + * we mean that this one calls all the others for a given derivation. + * + * This is a purely "administrative" goal type, which doesn't do any "real + * work". See `DerivationGoal` for what we mean by such an administrative goal. + * + * # Rationale + * + * It exists to solve two problems: + * + * 1. We want to build a derivation we don't yet have. + * + * Traditionally, that simply means we try to substitute the missing + * derivation; simple enough. However, with (currently experimental) + * dynamic derivations, derivations themselves can be the outputs of + * other derivations. That means the general case is that a + * `DerivationTrampolineGoal` needs to create *another* + * `DerivationTrampolineGoal` goal to realize the derivation it needs. + * That goal in turn might need to create a third + * `DerivationTrampolineGoal`, the induction down to a statically known + * derivation as the base case is arbitrary deep. + * + * 2. Only a subset of outputs is needed, but such subsets are discovered + * dynamically. + * + * Consider derivations: + * + * - A has outputs x, y, and z + * + * - B needs A^x,y + * + * - C needs A^y,z and B's single output + * + * With the current `Worker` architecture, we're first discover + * needing `A^y,z` and then discover needing `A^x,y`. Of course, we + * don't want to download `A^y` twice, either. + * + * The way we handle sharing work for `A^y` is to have + * `DerivationGoal` just handle a single output, and do slightly more + * work (though it is just an "administrative" goal too), and + * `DerivationTrampolineGoal` handle sets of goals, but have it (once the + * derivation itself has been gotten) *just* create + * `DerivationGoal`s. + * + * That means it is fine to create man `DerivationTrampolineGoal` with + * overlapping sets of outputs, because all the "real work" will be + * coordinated via `DerivationGoal`s, and sharing will be discovered. + * + * Both these problems *can* be solved by having just a more powerful + * `DerivationGoal`, but that makes `DerivationGoal` more complex. + * However the more complex `DerivationGoal` has these downsides: + * + * 1. It needs to cope with only sometimes knowing a `StorePath drvPath` + * (as opposed to a more general `SingleDerivedPath drvPath` with will + * be only resolved to a `StorePath` part way through the control flow). + * + * 2. It needs complicated "restarting logic" to cope with the set of + * "wanted outputs" growing over time. + * + * (1) is not so bad, but (2) is quite scary, and has been a source of + * bugs in the past. By splitting out `DerivationTrampolineGoal`, we + * crucially avoid a need for (2), letting goal sharing rather than + * ad-hoc retry mechanisms accomplish the deduplication we need. Solving + * (1) is just a by-product and extra bonus of creating + * `DerivationTrampolineGoal`. + * + * # Misc Notes + * + * If we already have the derivation (e.g. if the evaluator has created + * the derivation locally and then instructed the store to build it), we + * can skip the derivation-getting goal entirely as a small + * optimization. + */ +struct DerivationTrampolineGoal : public Goal +{ + /** + * How to obtain a store path of the derivation to build. + */ + ref drvReq; + + /** + * The specific outputs that we need to build. + */ + OutputsSpec wantedOutputs; + + DerivationTrampolineGoal( + ref drvReq, + const OutputsSpec & wantedOutputs, + Worker & worker, + BuildMode buildMode = bmNormal); + + DerivationTrampolineGoal( + const StorePath & drvPath, + const OutputsSpec & wantedOutputs, + const Derivation & drv, + Worker & worker, + BuildMode buildMode = bmNormal); + + virtual ~DerivationTrampolineGoal(); + + void timedOut(Error && ex) override; + + std::string key() override; + + JobCategory jobCategory() const override + { + return JobCategory::Administration; + }; + +private: + + BuildMode buildMode; + + Co init(); + Co haveDerivation(StorePath drvPath, Derivation drv); + + /** + * Shared between both constructors + */ + void commonInit(); +}; + +} diff --git a/src/libstore/build/drv-output-substitution-goal.hh b/src/libstore/include/nix/store/build/drv-output-substitution-goal.hh similarity index 77% rename from src/libstore/build/drv-output-substitution-goal.hh rename to src/libstore/include/nix/store/build/drv-output-substitution-goal.hh index 8c60d01987a..0176f001ab6 100644 --- a/src/libstore/build/drv-output-substitution-goal.hh +++ b/src/libstore/include/nix/store/build/drv-output-substitution-goal.hh @@ -4,10 +4,10 @@ #include #include -#include "store-api.hh" -#include "goal.hh" -#include "realisation.hh" -#include "muxable-pipe.hh" +#include "nix/store/store-api.hh" +#include "nix/store/build/goal.hh" +#include "nix/store/realisation.hh" +#include "nix/util/muxable-pipe.hh" namespace nix { @@ -33,8 +33,8 @@ public: typedef void (DrvOutputSubstitutionGoal::*GoalState)(); GoalState state; - Co init() override; - Co realisationFetched(std::shared_ptr outputInfo, nix::ref sub); + Co init(); + Co realisationFetched(Goals waitees, std::shared_ptr outputInfo, nix::ref sub); void timedOut(Error && ex) override { unreachable(); }; diff --git a/src/libstore/build/goal.hh b/src/libstore/include/nix/store/build/goal.hh similarity index 90% rename from src/libstore/build/goal.hh rename to src/libstore/include/nix/store/build/goal.hh index 1dd7ed52537..ee69c9cc7c8 100644 --- a/src/libstore/build/goal.hh +++ b/src/libstore/include/nix/store/build/goal.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "store-api.hh" -#include "build-result.hh" +#include "nix/store/store-api.hh" +#include "nix/store/build-result.hh" #include @@ -50,21 +50,33 @@ enum struct JobCategory { * A substitution an arbitrary store object; it will use network resources. */ Substitution, + /** + * A goal that does no "real" work by itself, and just exists to depend on + * other goals which *do* do real work. These goals therefore are not + * limited. + * + * These goals cannot infinitely create themselves, so there is no risk of + * a "fork bomb" type situation (which would be a problem even though the + * goal do no real work) either. + */ + Administration, }; struct Goal : public std::enable_shared_from_this { - typedef enum {ecBusy, ecSuccess, ecFailed, ecNoSubstituters, ecIncompleteClosure} ExitCode; - +private: /** - * Backlink to the worker. + * Goals that this goal is waiting for. */ - Worker & worker; + Goals waitees; + +public: + typedef enum {ecBusy, ecSuccess, ecFailed, ecNoSubstituters} ExitCode; /** - * Goals that this goal is waiting for. + * Backlink to the worker. */ - Goals waitees; + Worker & worker; /** * Goals waiting for this one to finish. Must use weak pointers @@ -83,12 +95,6 @@ struct Goal : public std::enable_shared_from_this */ size_t nrNoSubstituters = 0; - /** - * Number of substitution goals we are/were waiting for that - * failed because they had unsubstitutable references. - */ - size_t nrIncompleteClosure = 0; - /** * Name of this goal for debugging purposes. */ @@ -99,12 +105,10 @@ struct Goal : public std::enable_shared_from_this */ ExitCode exitCode = ecBusy; -protected: /** * Build result. */ BuildResult buildResult; -public: /** * Suspend our goal and wait until we get `work`-ed again. @@ -332,6 +336,7 @@ public: std::suspend_always await_transform(Suspend) { return {}; }; }; +protected: /** * The coroutine being currently executed. * MUST be updated when switching the coroutine being executed. @@ -341,17 +346,6 @@ public: */ std::optional top_co; - /** - * The entry point for the goal - */ - virtual Co init() = 0; - - /** - * Wrapper around @ref init since virtual functions - * can't be used in constructors. - */ - inline Co init_wrapper(); - /** * Signals that the goal is done. * `co_return` the result. If you're not inside a coroutine, you can ignore @@ -359,27 +353,27 @@ public: */ Done amDone(ExitCode result, std::optional ex = {}); +public: virtual void cleanup() { } /** - * Project a `BuildResult` with just the information that pertains - * to the given request. + * Hack to say that this goal should not log `ex`, but instead keep + * it around. Set by a waitee which sees itself as the designated + * continuation of this goal, responsible for reporting its + * successes or failures. * - * In general, goals may be aliased between multiple requests, and - * the stored `BuildResult` has information for the union of all - * requests. We don't want to leak what the other request are for - * sake of both privacy and determinism, and this "safe accessor" - * ensures we don't. + * @todo this is yet another not-nice hack in the goal system that + * we ought to get rid of. See #11927 */ - BuildResult getBuildResult(const DerivedPath &) const; + bool preserveException = false; /** * Exception containing an error message, if any. */ std::optional ex; - Goal(Worker & worker, DerivedPath path) - : worker(worker), top_co(init_wrapper()) + Goal(Worker & worker, Co init) + : worker(worker), top_co(std::move(init)) { // top_co shouldn't have a goal already, should be nullptr. assert(!top_co->handle.promise().goal); @@ -394,10 +388,6 @@ public: void work(); - void addWaitee(GoalPtr waitee); - - virtual void waiteeDone(GoalPtr waitee, ExitCode result); - virtual void handleChildOutput(Descriptor fd, std::string_view data) { unreachable(); @@ -429,6 +419,13 @@ public: * @see JobCategory */ virtual JobCategory jobCategory() const = 0; + +protected: + Co await(Goals waitees); + + Co waitForAWhile(); + Co waitForBuildSlot(); + Co yield(); }; void addToWeakGoals(WeakGoals & goals, GoalPtr p); @@ -439,7 +436,3 @@ template struct std::coroutine_traits { using promise_type = nix::Goal::promise_type; }; - -nix::Goal::Co nix::Goal::init_wrapper() { - co_return init(); -} diff --git a/src/libstore/build/substitution-goal.hh b/src/libstore/include/nix/store/build/substitution-goal.hh similarity index 92% rename from src/libstore/build/substitution-goal.hh rename to src/libstore/include/nix/store/build/substitution-goal.hh index f2cf797e5d2..b61706840f2 100644 --- a/src/libstore/build/substitution-goal.hh +++ b/src/libstore/include/nix/store/build/substitution-goal.hh @@ -1,10 +1,10 @@ #pragma once ///@file -#include "worker.hh" -#include "store-api.hh" -#include "goal.hh" -#include "muxable-pipe.hh" +#include "nix/store/build/worker.hh" +#include "nix/store/store-api.hh" +#include "nix/store/build/goal.hh" +#include "nix/util/muxable-pipe.hh" #include #include #include @@ -64,7 +64,7 @@ public: /** * The states. */ - Co init() override; + Co init(); Co gotInfo(); Co tryToRun(StorePath subPath, nix::ref sub, std::shared_ptr info, bool & substituterFailed); Co finished(); diff --git a/src/libstore/build/worker.hh b/src/libstore/include/nix/store/build/worker.hh similarity index 84% rename from src/libstore/build/worker.hh rename to src/libstore/include/nix/store/build/worker.hh index f5e61720807..491b8f4941e 100644 --- a/src/libstore/build/worker.hh +++ b/src/libstore/include/nix/store/build/worker.hh @@ -1,11 +1,12 @@ #pragma once ///@file -#include "types.hh" -#include "store-api.hh" -#include "goal.hh" -#include "realisation.hh" -#include "muxable-pipe.hh" +#include "nix/util/types.hh" +#include "nix/store/store-api.hh" +#include "nix/store/derived-path-map.hh" +#include "nix/store/build/goal.hh" +#include "nix/store/realisation.hh" +#include "nix/util/muxable-pipe.hh" #include #include @@ -13,7 +14,9 @@ namespace nix { /* Forward definition. */ +struct DerivationTrampolineGoal; struct DerivationGoal; +struct DerivationBuildingGoal; struct PathSubstitutionGoal; class DrvOutputSubstitutionGoal; @@ -31,6 +34,7 @@ class DrvOutputSubstitutionGoal; */ GoalPtr upcast_goal(std::shared_ptr subGoal); GoalPtr upcast_goal(std::shared_ptr subGoal); +GoalPtr upcast_goal(std::shared_ptr subGoal); typedef std::chrono::time_point steady_time_point; @@ -103,7 +107,11 @@ private: * Maps used to prevent multiple instantiations of a goal for the * same derivation / path. */ - std::map> derivationGoals; + + DerivedPathMap>> derivationTrampolineGoals; + + std::map>> derivationGoals; + std::map> derivationBuildingGoals; std::map> substitutionGoals; std::map> drvOutputSubstitutionGoals; @@ -196,16 +204,30 @@ public: * @ref DerivationGoal "derivation goal" */ private: - std::shared_ptr makeDerivationGoalCommon( - const StorePath & drvPath, const OutputsSpec & wantedOutputs, - std::function()> mkDrvGoal); + template + std::shared_ptr initGoalIfNeeded(std::weak_ptr & goal_weak, Args && ...args); + + std::shared_ptr makeDerivationTrampolineGoal( + ref drvReq, + const OutputsSpec & wantedOutputs, BuildMode buildMode = bmNormal); + public: - std::shared_ptr makeDerivationGoal( + std::shared_ptr makeDerivationTrampolineGoal( const StorePath & drvPath, - const OutputsSpec & wantedOutputs, BuildMode buildMode = bmNormal); - std::shared_ptr makeBasicDerivationGoal( - const StorePath & drvPath, const BasicDerivation & drv, - const OutputsSpec & wantedOutputs, BuildMode buildMode = bmNormal); + const OutputsSpec & wantedOutputs, + const Derivation & drv, + BuildMode buildMode = bmNormal); + + std::shared_ptr makeDerivationGoal( + const StorePath & drvPath, const Derivation & drv, + const OutputName & wantedOutput, BuildMode buildMode = bmNormal); + + /** + * @ref DerivationBuildingGoal "derivation goal" + */ + std::shared_ptr makeDerivationBuildingGoal( + const StorePath & drvPath, const Derivation & drv, + BuildMode buildMode = bmNormal); /** * @ref PathSubstitutionGoal "substitution goal" @@ -217,7 +239,7 @@ public: * Make a goal corresponding to the `DerivedPath`. * * It will be a `DerivationGoal` for a `DerivedPath::Built` or - * a `SubstitutionGoal` for a `DerivedPath::Opaque`. + * a `PathSubstitutionGoal` for a `DerivedPath::Opaque`. */ GoalPtr makeGoal(const DerivedPath & req, BuildMode buildMode = bmNormal); diff --git a/src/libstore/include/nix/store/builtins.hh b/src/libstore/include/nix/store/builtins.hh new file mode 100644 index 00000000000..096c8af7bc8 --- /dev/null +++ b/src/libstore/include/nix/store/builtins.hh @@ -0,0 +1,34 @@ +#pragma once +///@file + +#include "nix/store/derivations.hh" + +namespace nix { + +struct BuiltinBuilderContext +{ + const BasicDerivation & drv; + std::map outputs; + std::string netrcData; + std::string caFileData; + Path tmpDirInSandbox; +}; + +using BuiltinBuilder = std::function; + +struct RegisterBuiltinBuilder +{ + typedef std::map BuiltinBuilders; + + static BuiltinBuilders & builtinBuilders() { + static BuiltinBuilders builders; + return builders; + } + + RegisterBuiltinBuilder(const std::string & name, BuiltinBuilder && fun) + { + builtinBuilders().insert_or_assign(name, std::move(fun)); + } +}; + +} diff --git a/src/libstore/builtins/buildenv.hh b/src/libstore/include/nix/store/builtins/buildenv.hh similarity index 88% rename from src/libstore/builtins/buildenv.hh rename to src/libstore/include/nix/store/builtins/buildenv.hh index 8e112e176e2..163666c0bd4 100644 --- a/src/libstore/builtins/buildenv.hh +++ b/src/libstore/include/nix/store/builtins/buildenv.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "store-api.hh" +#include "nix/store/store-api.hh" namespace nix { @@ -45,8 +45,4 @@ typedef std::vector Packages; void buildProfile(const Path & out, Packages && pkgs); -void builtinBuildenv( - const BasicDerivation & drv, - const std::map & outputs); - } diff --git a/src/libstore/common-protocol-impl.hh b/src/libstore/include/nix/store/common-protocol-impl.hh similarity index 78% rename from src/libstore/common-protocol-impl.hh rename to src/libstore/include/nix/store/common-protocol-impl.hh index 360882c0289..e9c726a994d 100644 --- a/src/libstore/common-protocol-impl.hh +++ b/src/libstore/include/nix/store/common-protocol-impl.hh @@ -4,12 +4,12 @@ * * Template implementations (as opposed to mere declarations). * - * This file is an exmample of the "impl.hh" pattern. See the + * This file is an example of the "impl.hh" pattern. See the * contributing guide. */ -#include "common-protocol.hh" -#include "length-prefixed-protocol-helper.hh" +#include "nix/store/common-protocol.hh" +#include "nix/store/length-prefixed-protocol-helper.hh" namespace nix { @@ -25,11 +25,11 @@ namespace nix { LengthPrefixedProtoHelper::write(store, conn, t); \ } +#define COMMA_ , COMMON_USE_LENGTH_PREFIX_SERIALISER(template, std::vector) -COMMON_USE_LENGTH_PREFIX_SERIALISER(template, std::set) +COMMON_USE_LENGTH_PREFIX_SERIALISER(template, std::set) COMMON_USE_LENGTH_PREFIX_SERIALISER(template, std::tuple) -#define COMMA_ , COMMON_USE_LENGTH_PREFIX_SERIALISER( template, std::map) diff --git a/src/libstore/common-protocol.hh b/src/libstore/include/nix/store/common-protocol.hh similarity index 91% rename from src/libstore/common-protocol.hh rename to src/libstore/include/nix/store/common-protocol.hh index a878e84c9d8..1dc4aa7c569 100644 --- a/src/libstore/common-protocol.hh +++ b/src/libstore/include/nix/store/common-protocol.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "serialise.hh" +#include "nix/util/serialise.hh" namespace nix { @@ -72,14 +72,14 @@ DECLARE_COMMON_SERIALISER(DrvOutput); template<> DECLARE_COMMON_SERIALISER(Realisation); +#define COMMA_ , template DECLARE_COMMON_SERIALISER(std::vector); -template -DECLARE_COMMON_SERIALISER(std::set); +template +DECLARE_COMMON_SERIALISER(std::set); template DECLARE_COMMON_SERIALISER(std::tuple); -#define COMMA_ , template DECLARE_COMMON_SERIALISER(std::map); #undef COMMA_ @@ -89,12 +89,12 @@ DECLARE_COMMON_SERIALISER(std::map); * that the underlying types never serialize to the empty string. * * We do this instead of a generic std::optional instance because - * ordinal tags (0 or 1, here) are a bit of a compatability hazard. For + * ordinal tags (0 or 1, here) are a bit of a compatibility hazard. For * the same reason, we don't have a std::variant instances (ordinal * tags 0...n). * * We could the generic instances and then these as specializations for - * compatability, but that's proven a bit finnicky, and also makes the + * compatibility, but that's proven a bit finnicky, and also makes the * worker protocol harder to implement in other languages where such * specializations may not be allowed. */ diff --git a/src/libstore/common-ssh-store-config.hh b/src/libstore/include/nix/store/common-ssh-store-config.hh similarity index 95% rename from src/libstore/common-ssh-store-config.hh rename to src/libstore/include/nix/store/common-ssh-store-config.hh index 5deb6f4c9e9..82a78f0755a 100644 --- a/src/libstore/common-ssh-store-config.hh +++ b/src/libstore/include/nix/store/common-ssh-store-config.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "store-api.hh" +#include "nix/store/store-api.hh" namespace nix { @@ -56,7 +56,7 @@ struct CommonSSHStoreConfig : virtual StoreConfig */ SSHMaster createSSHMaster( bool useMaster, - Descriptor logFD = INVALID_DESCRIPTOR); + Descriptor logFD = INVALID_DESCRIPTOR) const; }; } diff --git a/src/libstore/content-address.hh b/src/libstore/include/nix/store/content-address.hh similarity index 98% rename from src/libstore/content-address.hh rename to src/libstore/include/nix/store/content-address.hh index 2b5d1296a27..8442fabb27e 100644 --- a/src/libstore/content-address.hh +++ b/src/libstore/include/nix/store/content-address.hh @@ -2,10 +2,10 @@ ///@file #include -#include "hash.hh" -#include "path.hh" -#include "file-content-address.hh" -#include "variant-wrapper.hh" +#include "nix/util/hash.hh" +#include "nix/store/path.hh" +#include "nix/util/file-content-address.hh" +#include "nix/util/variant-wrapper.hh" namespace nix { diff --git a/src/libstore/daemon.hh b/src/libstore/include/nix/store/daemon.hh similarity index 79% rename from src/libstore/daemon.hh rename to src/libstore/include/nix/store/daemon.hh index a8ce32d8deb..d14541df761 100644 --- a/src/libstore/daemon.hh +++ b/src/libstore/include/nix/store/daemon.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "serialise.hh" -#include "store-api.hh" +#include "nix/util/serialise.hh" +#include "nix/store/store-api.hh" namespace nix::daemon { diff --git a/src/libstore/include/nix/store/derivation-options.hh b/src/libstore/include/nix/store/derivation-options.hh new file mode 100644 index 00000000000..f61a43e6031 --- /dev/null +++ b/src/libstore/include/nix/store/derivation-options.hh @@ -0,0 +1,207 @@ +#pragma once +///@file + +#include +#include +#include +#include + +#include "nix/util/types.hh" +#include "nix/util/json-impls.hh" + +namespace nix { + +class Store; +struct BasicDerivation; +struct StructuredAttrs; + +/** + * This represents all the special options on a `Derivation`. + * + * Currently, these options are parsed from the environment variables + * with the aid of `StructuredAttrs`. + * + * The first goal of this data type is to make sure that no other code + * uses `StructuredAttrs` to ad-hoc parse some additional options. That + * ensures this data type is up to date and fully correct. + * + * The second goal of this data type is to allow an alternative to + * hackily parsing the options from the environment variables. The ATerm + * format cannot change, but in alternatives to it (like the JSON + * format), we have the option of instead storing the options + * separately. That would be nice to separate concerns, and not make any + * environment variable names magical. + */ +struct DerivationOptions +{ + struct OutputChecks + { + bool ignoreSelfRefs = false; + std::optional maxSize, maxClosureSize; + + /** + * env: allowedReferences + * + * A value of `nullopt` indicates that the check is skipped. + * This means that all references are allowed. + */ + std::optional allowedReferences; + + /** + * env: disallowedReferences + * + * No needed for `std::optional`, because skipping the check is + * the same as disallowing the references. + */ + StringSet disallowedReferences; + + /** + * env: allowedRequisites + * + * See `allowedReferences` + */ + std::optional allowedRequisites; + + /** + * env: disallowedRequisites + * + * See `disallowedReferences` + */ + StringSet disallowedRequisites; + + bool operator==(const OutputChecks &) const = default; + }; + + /** + * Either one set of checks for all outputs, or separate checks + * per-output. + */ + std::variant> outputChecks = OutputChecks{}; + + /** + * Whether to avoid scanning for references for a given output. + */ + std::map unsafeDiscardReferences; + + /** + * In non-structured mode, all bindings specified in the derivation + * go directly via the environment, except those listed in the + * passAsFile attribute. Those are instead passed as file names + * pointing to temporary files containing the contents. + * + * Note that passAsFile is ignored in structure mode because it's + * not needed (attributes are not passed through the environment, so + * there is no size constraint). + */ + StringSet passAsFile; + + /** + * The `exportReferencesGraph' feature allows the references graph + * to be passed to a builder + * + * ### Legacy case + * + * Given a `name` `pathSet` key-value pair, the references graph of + * `pathSet` will be stored in a text file `name' in the temporary + * build directory. The text files have the format used by + * `nix-store + * --register-validity'. However, the `deriver` fields are left + * empty. + * + * ### "Structured attributes" case + * + * The same information will be put put in the final structured + * attributes give to the builder. The set of paths in the original JSON + * is replaced with a list of `PathInfo` in JSON format. + */ + std::map exportReferencesGraph; + + /** + * env: __sandboxProfile + * + * Just for Darwin + */ + std::string additionalSandboxProfile = ""; + + /** + * env: __noChroot + * + * Derivation would like to opt out of the sandbox. + * + * Builder is free to not respect this wish (because it is + * insecure) and fail the build instead. + */ + bool noChroot = false; + + /** + * env: __impureHostDeps + */ + StringSet impureHostDeps = {}; + + /** + * env: impureEnvVars + */ + StringSet impureEnvVars = {}; + + /** + * env: __darwinAllowLocalNetworking + * + * Just for Darwin + */ + bool allowLocalNetworking = false; + + /** + * env: requiredSystemFeatures + */ + StringSet requiredSystemFeatures = {}; + + /** + * env: preferLocalBuild + */ + bool preferLocalBuild = false; + + /** + * env: allowSubstitutes + */ + bool allowSubstitutes = true; + + bool operator==(const DerivationOptions &) const = default; + + /** + * Parse this information from its legacy encoding as part of the + * environment. This should not be used with nice greenfield formats + * (e.g. JSON) but is necessary for supporting old formats (e.g. + * ATerm). + */ + static DerivationOptions + fromStructuredAttrs(const StringMap & env, const StructuredAttrs * parsed, bool shouldWarn = true); + + /** + * @param drv Must be the same derivation we parsed this from. In + * the future we'll flip things around so a `BasicDerivation` has + * `DerivationOptions` instead. + */ + StringSet getRequiredSystemFeatures(const BasicDerivation & drv) const; + + /** + * @param drv See note on `getRequiredSystemFeatures` + */ + bool canBuildLocally(Store & localStore, const BasicDerivation & drv) const; + + /** + * @param drv See note on `getRequiredSystemFeatures` + */ + bool willBuildLocally(Store & localStore, const BasicDerivation & drv) const; + + bool substitutesAllowed() const; + + /** + * @param drv See note on `getRequiredSystemFeatures` + */ + bool useUidRange(const BasicDerivation & drv) const; +}; + +}; + +JSON_IMPL(DerivationOptions); +JSON_IMPL(DerivationOptions::OutputChecks) diff --git a/src/libstore/derivations.hh b/src/libstore/include/nix/store/derivations.hh similarity index 96% rename from src/libstore/derivations.hh rename to src/libstore/include/nix/store/derivations.hh index 765b66ade2a..a813137bcba 100644 --- a/src/libstore/derivations.hh +++ b/src/libstore/include/nix/store/derivations.hh @@ -1,14 +1,14 @@ #pragma once ///@file -#include "path.hh" -#include "types.hh" -#include "hash.hh" -#include "content-address.hh" -#include "repair-flag.hh" -#include "derived-path-map.hh" -#include "sync.hh" -#include "variant-wrapper.hh" +#include "nix/store/path.hh" +#include "nix/util/types.hh" +#include "nix/util/hash.hh" +#include "nix/store/content-address.hh" +#include "nix/util/repair-flag.hh" +#include "nix/store/derived-path-map.hh" +#include "nix/util/sync.hh" +#include "nix/util/variant-wrapper.hh" #include #include @@ -187,7 +187,7 @@ struct DerivationType { }; /** - * Content-addressed derivation types + * Content-addressing derivation types */ struct ContentAddressed { /** @@ -214,7 +214,7 @@ struct DerivationType { /** * Impure derivation type * - * This is similar at buil-time to the content addressed, not standboxed, not fixed + * This is similar at build-time to the content addressed, not standboxed, not fixed * type, but has some restrictions on its usage. */ struct Impure { @@ -343,7 +343,7 @@ struct Derivation : BasicDerivation /** * inputs that are sub-derivations */ - DerivedPathMap> inputDrvs; + DerivedPathMap>> inputDrvs; /** * Print a derivation. @@ -369,7 +369,7 @@ struct Derivation : BasicDerivation */ std::optional tryResolve( Store & store, - const std::map, StorePath> & inputDrvOutputs) const; + std::function(ref drvPath, const std::string & outputName)> queryResolutionChain) const; /** * Check that the derivation is valid and does not present any @@ -526,6 +526,4 @@ void writeDerivation(Sink & out, const StoreDirConfig & store, const BasicDeriva */ std::string hashPlaceholder(const OutputNameView outputName); -extern const Hash impureOutputHash; - } diff --git a/src/libstore/derived-path-map.hh b/src/libstore/include/nix/store/derived-path-map.hh similarity index 77% rename from src/libstore/derived-path-map.hh rename to src/libstore/include/nix/store/derived-path-map.hh index bd60fe88710..6dae73fab3a 100644 --- a/src/libstore/derived-path-map.hh +++ b/src/libstore/include/nix/store/derived-path-map.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "types.hh" -#include "derived-path.hh" +#include "nix/util/types.hh" +#include "nix/store/derived-path.hh" namespace nix { @@ -21,8 +21,11 @@ namespace nix { * * @param V A type to instantiate for each output. It should probably * should be an "optional" type so not every interior node has to have a - * value. `* const Something` or `std::optional` would be - * good choices for "optional" types. + * value. For example, the scheduler uses + * `DerivedPathMap>` to + * remember which goals correspond to which outputs. `* const Something` + * or `std::optional` would also be good choices for + * "optional" types. */ template struct DerivedPathMap { @@ -91,20 +94,20 @@ struct DerivedPathMap { }; template<> -bool DerivedPathMap>::ChildNode::operator == ( - const DerivedPathMap>::ChildNode &) const noexcept; +bool DerivedPathMap::ChildNode::operator == ( + const DerivedPathMap::ChildNode &) const noexcept; // TODO libc++ 16 (used by darwin) missing `std::map::operator <=>`, can't do yet. #if 0 template<> -std::strong_ordering DerivedPathMap>::ChildNode::operator <=> ( - const DerivedPathMap>::ChildNode &) const noexcept; +std::strong_ordering DerivedPathMap::ChildNode::operator <=> ( + const DerivedPathMap::ChildNode &) const noexcept; template<> -inline auto DerivedPathMap>::operator <=> (const DerivedPathMap> &) const noexcept = default; +inline auto DerivedPathMap::operator <=> (const DerivedPathMap &) const noexcept = default; #endif -extern template struct DerivedPathMap>::ChildNode; -extern template struct DerivedPathMap>; +extern template struct DerivedPathMap::ChildNode; +extern template struct DerivedPathMap; } diff --git a/src/libstore/derived-path.hh b/src/libstore/include/nix/store/derived-path.hh similarity index 96% rename from src/libstore/derived-path.hh rename to src/libstore/include/nix/store/derived-path.hh index 4ba3fb37d4c..64189bd41cb 100644 --- a/src/libstore/derived-path.hh +++ b/src/libstore/include/nix/store/derived-path.hh @@ -1,10 +1,10 @@ #pragma once ///@file -#include "path.hh" -#include "outputs-spec.hh" -#include "config.hh" -#include "ref.hh" +#include "nix/store/path.hh" +#include "nix/store/outputs-spec.hh" +#include "nix/util/configuration.hh" +#include "nix/util/ref.hh" #include @@ -45,7 +45,7 @@ struct SingleDerivedPath; * path of the given output name. */ struct SingleDerivedPathBuilt { - ref drvPath; + ref drvPath; OutputName output; /** @@ -74,7 +74,7 @@ struct SingleDerivedPathBuilt { * @param xpSettings Stop-gap to avoid globals during unit tests. */ static SingleDerivedPathBuilt parse( - const StoreDirConfig & store, ref drvPath, + const StoreDirConfig & store, ref drvPath, OutputNameView outputs, const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); nlohmann::json toJSON(Store & store) const; @@ -172,7 +172,7 @@ static inline ref makeConstantStorePathRef(StorePath drvPath) * output name. */ struct DerivedPathBuilt { - ref drvPath; + ref drvPath; OutputsSpec outputs; /** @@ -201,7 +201,7 @@ struct DerivedPathBuilt { * @param xpSettings Stop-gap to avoid globals during unit tests. */ static DerivedPathBuilt parse( - const StoreDirConfig & store, ref, + const StoreDirConfig & store, ref, std::string_view, const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); nlohmann::json toJSON(Store & store) const; diff --git a/src/libstore/downstream-placeholder.hh b/src/libstore/include/nix/store/downstream-placeholder.hh similarity index 97% rename from src/libstore/downstream-placeholder.hh rename to src/libstore/include/nix/store/downstream-placeholder.hh index c911ecea2ed..da03cd9a61b 100644 --- a/src/libstore/downstream-placeholder.hh +++ b/src/libstore/include/nix/store/downstream-placeholder.hh @@ -1,9 +1,9 @@ #pragma once ///@file -#include "hash.hh" -#include "path.hh" -#include "derived-path.hh" +#include "nix/util/hash.hh" +#include "nix/store/path.hh" +#include "nix/store/derived-path.hh" namespace nix { diff --git a/src/libstore/filetransfer.hh b/src/libstore/include/nix/store/filetransfer.hh similarity index 88% rename from src/libstore/filetransfer.hh rename to src/libstore/include/nix/store/filetransfer.hh index d836ab2c4a7..745aeb29ee3 100644 --- a/src/libstore/filetransfer.hh +++ b/src/libstore/include/nix/store/filetransfer.hh @@ -4,11 +4,11 @@ #include #include -#include "logging.hh" -#include "types.hh" -#include "ref.hh" -#include "config.hh" -#include "serialise.hh" +#include "nix/util/logging.hh" +#include "nix/util/types.hh" +#include "nix/util/ref.hh" +#include "nix/util/configuration.hh" +#include "nix/util/serialise.hh" namespace nix { @@ -30,7 +30,7 @@ struct FileTransferSettings : Config {"binary-caches-parallel-connections"}}; Setting connectTimeout{ - this, 0, "connect-timeout", + this, 5, "connect-timeout", R"( The timeout (in seconds) for establishing connections in the binary cache substituter. It corresponds to `curl`’s @@ -46,17 +46,20 @@ struct FileTransferSettings : Config )"}; Setting tries{this, 5, "download-attempts", - "How often Nix will attempt to download a file before giving up."}; + "The number of times Nix attempts to download a file before giving up."}; Setting downloadBufferSize{this, 64 * 1024 * 1024, "download-buffer-size", R"( - The size of Nix's internal download buffer during `curl` transfers. If data is + The size of Nix's internal download buffer in bytes during `curl` transfers. If data is not processed quickly enough to exceed the size of this buffer, downloads may stall. + The default is 67108864 (64 MiB). )"}; }; extern FileTransferSettings fileTransferSettings; +extern const unsigned int RETRY_TIME_MS_DEFAULT; + struct FileTransferRequest { std::string uri; @@ -64,8 +67,9 @@ struct FileTransferRequest std::string expectedETag; bool verifyTLS = true; bool head = false; + bool post = false; size_t tries = fileTransferSettings.tries; - unsigned int baseRetryTimeMs = 250; + unsigned int baseRetryTimeMs = RETRY_TIME_MS_DEFAULT; ActivityId parentAct; bool decompress = true; std::optional data; @@ -75,7 +79,7 @@ struct FileTransferRequest FileTransferRequest(std::string_view uri) : uri(uri), parentAct(getCurActivity()) { } - std::string verb() + std::string verb() const { return data ? "upload" : "download"; } diff --git a/src/libstore/gc-store.hh b/src/libstore/include/nix/store/gc-store.hh similarity index 98% rename from src/libstore/gc-store.hh rename to src/libstore/include/nix/store/gc-store.hh index 020f770b07a..8b25ec8d4cb 100644 --- a/src/libstore/gc-store.hh +++ b/src/libstore/include/nix/store/gc-store.hh @@ -3,7 +3,7 @@ #include -#include "store-api.hh" +#include "nix/store/store-api.hh" namespace nix { @@ -89,7 +89,7 @@ struct GCResults * Some views have only a no-op temp roots even though others to the * same store allow triggering GC. For instance one can't add a root * over ssh, but that doesn't prevent someone from gc-ing that store - * accesed via SSH locally). + * accessed via SSH locally). * * - The derived `LocalFSStore` class has `LocalFSStore::addPermRoot`, * which is not part of this class because it relies on the notion of diff --git a/src/libstore/globals.hh b/src/libstore/include/nix/store/globals.hh similarity index 87% rename from src/libstore/globals.hh rename to src/libstore/include/nix/store/globals.hh index ff3df46ba9e..3f9d8697e45 100644 --- a/src/libstore/globals.hh +++ b/src/libstore/include/nix/store/globals.hh @@ -1,17 +1,19 @@ #pragma once ///@file -#include "types.hh" -#include "config.hh" -#include "environment-variables.hh" -#include "experimental-features.hh" -#include "users.hh" - #include #include #include +#include "nix/util/types.hh" +#include "nix/util/configuration.hh" +#include "nix/util/environment-variables.hh" +#include "nix/util/experimental-features.hh" +#include "nix/util/users.hh" + +#include "nix/store/config.hh" + namespace nix { typedef enum { smEnabled, smRelaxed, smDisabled } SandboxMode; @@ -22,7 +24,7 @@ struct MaxBuildJobsSetting : public BaseSetting unsigned int def, const std::string & name, const std::string & description, - const std::set & aliases = {}) + const StringSet & aliases = {}) : BaseSetting(def, true, name, description, aliases) { options->addSetting(this); @@ -32,7 +34,7 @@ struct MaxBuildJobsSetting : public BaseSetting }; const uint32_t maxIdsPerBuild = - #if __linux__ + #ifdef __linux__ 1 << 16 #else 1 @@ -41,8 +43,6 @@ const uint32_t maxIdsPerBuild = class Settings : public Config { - unsigned int getDefaultCores(); - StringSet getDefaultSystemFeatures(); StringSet getDefaultExtraPlatforms(); @@ -55,6 +55,8 @@ public: Settings(); + unsigned int getDefaultCores() const; + Path nixPrefix; /** @@ -84,11 +86,6 @@ public: */ std::vector nixUserConfFiles; - /** - * The directory where the man pages are stored. - */ - Path nixManDir; - /** * File name of the socket the daemon listens to. */ @@ -112,7 +109,7 @@ public: Setting tryFallback{ this, false, "fallback", R"( - If set to `true`, Nix will fall back to building from source if a + If set to `true`, Nix falls back to building from source if a binary substitute fails. This is equivalent to the `--fallback` flag. The default is `false`. )", @@ -130,11 +127,11 @@ public: MaxBuildJobsSetting maxBuildJobs{ this, 1, "max-jobs", R"( - Maximum number of jobs that Nix will try to build locally in parallel. + Maximum number of jobs that Nix tries to build locally in parallel. The special value `auto` causes Nix to use the number of CPUs in your system. Use `0` to disable local builds and directly use the remote machines specified in [`builders`](#conf-builders). - This will not affect derivations that have [`preferLocalBuild = true`](@docroot@/language/advanced-attributes.md#adv-attr-preferLocalBuild), which are always built locally. + This doesn't affect derivations that have [`preferLocalBuild = true`](@docroot@/language/advanced-attributes.md#adv-attr-preferLocalBuild), which are always built locally. > **Note** > @@ -149,14 +146,14 @@ public: this, 16, "max-substitution-jobs", R"( This option defines the maximum number of substitution jobs that Nix - will try to run in parallel. The default is `16`. The minimum value - one can choose is `1` and lower values will be interpreted as `1`. + tries to run in parallel. The default is `16`. The minimum value + one can choose is `1` and lower values are interpreted as `1`. )", {"substitution-max-jobs"}}; Setting buildCores{ this, - getDefaultCores(), + 0, "cores", R"( Sets the value of the `NIX_BUILD_CORES` environment variable in the [invocation of the `builder` executable](@docroot@/language/derivations.md#builder-execution) of a derivation. @@ -167,17 +164,15 @@ public: A very generic example using `derivation` and `xargs` may be more appropriate to explain the mechanism. Using `mkDerivation` as an example requires being aware of that there are multiple independent layers that are completely opaque here. --> - For instance, in Nixpkgs, if the attribute `enableParallelBuilding` for the `mkDerivation` build helper is set to `true`, it will pass the `-j${NIX_BUILD_CORES}` flag to GNU Make. + For instance, in Nixpkgs, if the attribute `enableParallelBuilding` for the `mkDerivation` build helper is set to `true`, it passes the `-j${NIX_BUILD_CORES}` flag to GNU Make. - The value `0` means that the `builder` should use all available CPU cores in the system. + If set to `0`, nix will detect the number of CPU cores and pass this number via NIX_BUILD_CORES. > **Note** > > The number of parallel local Nix build jobs is independently controlled with the [`max-jobs`](#conf-max-jobs) setting. )", - {"build-cores"}, - // Don't document the machine-specific default value - false}; + {"build-cores"}}; /** * Read-only mode. Don't copy stuff to the store, don't change @@ -186,10 +181,10 @@ public: bool readOnlyMode = false; Setting thisSystem{ - this, SYSTEM, "system", + this, NIX_LOCAL_SYSTEM, "system", R"( The system type of the current Nix installation. - Nix will only build a given [derivation](@docroot@/language/derivations.md) locally when its `system` attribute equals any of the values specified here or in [`extra-platforms`](#conf-extra-platforms). + Nix only builds a given [store derivation](@docroot@/glossary.md#gloss-store-derivation) locally when its `system` attribute equals any of the values specified here or in [`extra-platforms`](#conf-extra-platforms). The default value is set when Nix itself is compiled for the system it will run on. The following system types are widely used, as Nix is actively supported on these platforms: @@ -295,28 +290,28 @@ public: > `i686-linux,x86_64-linux` 3. The SSH identity file to be used to log in to the remote machine. - If omitted, SSH will use its regular identities. + If omitted, SSH uses its regular identities. > **Example** > > `/home/user/.ssh/id_mac` - 4. The maximum number of builds that Nix will execute in parallel on the machine. + 4. The maximum number of builds that Nix executes in parallel on the machine. Typically this should be equal to the number of CPU cores. 5. The “speed factorâ€, indicating the relative speed of the machine as a positive integer. - If there are multiple machines of the right type, Nix will prefer the fastest, taking load into account. + If there are multiple machines of the right type, Nix prefers the fastest, taking load into account. 6. A comma-separated list of supported [system features](#conf-system-features). - A machine will only be used to build a derivation if all the features in the derivation's [`requiredSystemFeatures`](@docroot@/language/advanced-attributes.html#adv-attr-requiredSystemFeatures) attribute are supported by that machine. + A machine is only used to build a derivation if all the features in the derivation's [`requiredSystemFeatures`](@docroot@/language/advanced-attributes.html#adv-attr-requiredSystemFeatures) attribute are supported by that machine. 7. A comma-separated list of required [system features](#conf-system-features). - A machine will only be used to build a derivation if all of the machine’s required features appear in the derivation’s [`requiredSystemFeatures`](@docroot@/language/advanced-attributes.html#adv-attr-requiredSystemFeatures) attribute. + A machine is only used to build a derivation if all of the machine’s required features appear in the derivation’s [`requiredSystemFeatures`](@docroot@/language/advanced-attributes.html#adv-attr-requiredSystemFeatures) attribute. 8. The (base64-encoded) public host key of the remote machine. - If omitted, SSH will use its regular `known_hosts` file. + If omitted, SSH uses its regular `known_hosts` file. The value for this field can be obtained via `base64 -w0`. @@ -338,7 +333,7 @@ public: > nix@poochie.labs.cs.uu.nl i686-linux /home/nix/.ssh/id_scratchy 1 2 kvm benchmark > ``` > - > However, `poochie` will only build derivations that have the attribute + > However, `poochie` only builds derivations that have the attribute > > ```nix > requiredSystemFeatures = [ "benchmark" ]; @@ -351,7 +346,7 @@ public: > ``` > > `itchy` cannot do builds that require `kvm`, but `scratchy` does support such builds. - > For regular builds, `itchy` will be preferred over `scratchy` because it has a higher speed factor. + > For regular builds, `itchy` is preferred over `scratchy` because it has a higher speed factor. For Nix to use substituters, the calling user must be in the [`trusted-users`](#conf-trusted-users) list. @@ -368,22 +363,22 @@ public: To build only on remote machines and disable local builds, set [`max-jobs`](#conf-max-jobs) to 0. - If you want the remote machines to use substituters, set [`builders-use-substitutes`](#conf-builders-use-substituters) to `true`. + If you want the remote machines to use substituters, set [`builders-use-substitutes`](#conf-builders-use-substitutes) to `true`. )", {}, false}; Setting alwaysAllowSubstitutes{ this, false, "always-allow-substitutes", R"( - If set to `true`, Nix will ignore the [`allowSubstitutes`](@docroot@/language/advanced-attributes.md) attribute in derivations and always attempt to use [available substituters](#conf-substituters). + If set to `true`, Nix ignores the [`allowSubstitutes`](@docroot@/language/advanced-attributes.md) attribute in derivations and always attempt to use [available substituters](#conf-substituters). )"}; Setting buildersUseSubstitutes{ this, false, "builders-use-substitutes", R"( - If set to `true`, Nix will instruct [remote build machines](#conf-builders) to use their own [`substituters`](#conf-substituters) if available. + If set to `true`, Nix instructs [remote build machines](#conf-builders) to use their own [`substituters`](#conf-substituters) if available. - It means that remote build hosts will fetch as many dependencies as possible from their own substituters (e.g, from `cache.nixos.org`) instead of waiting for the local machine to upload them all. + It means that remote build hosts fetches as many dependencies as possible from their own substituters (e.g, from `cache.nixos.org`) instead of waiting for the local machine to upload them all. This can drastically reduce build times if the network connection between the local machine and the remote build host is slow. )"}; @@ -418,7 +413,7 @@ public: Setting useSubstitutes{ this, true, "substitute", R"( - If set to `true` (default), Nix will use binary substitutes if + If set to `true` (default), Nix uses binary substitutes if available. This option can be disabled to force building from source. )", @@ -435,11 +430,11 @@ public: since that would allow him/her to influence the build result. Therefore, if this option is non-empty and specifies a valid group, - builds will be performed under the user accounts that are a member + builds are performed under the user accounts that are a member of the group specified here (as listed in `/etc/group`). Those user accounts should not be used for any other purpose\! - Nix will never run two builds under the same user account at the + Nix never runs two builds under the same user account at the same time. This is to prevent an obvious security hole: a malicious user writing a Nix expression that modifies the build result of a legitimate Nix expression being built by another user. Therefore it @@ -451,7 +446,7 @@ public: by the Nix account, its group should be the group specified here, and its mode should be `1775`. - If the build users group is empty, builds will be performed under + If the build users group is empty, builds areperformed under the uid of the Nix process (that is, the uid of the caller if `NIX_REMOTE` is empty, the uid under which the Nix daemon runs if `NIX_REMOTE` is `daemon`). Obviously, this should not be used @@ -470,7 +465,7 @@ public: )", {}, true, Xp::AutoAllocateUids}; Setting startId{this, - #if __linux__ + #ifdef __linux__ 0x34000000, #else 56930, @@ -479,7 +474,7 @@ public: "The first UID and GID to use for dynamic ID allocation."}; Setting uidCount{this, - #if __linux__ + #ifdef __linux__ maxIdsPerBuild * 128, #else 128, @@ -487,7 +482,7 @@ public: "id-count", "The number of UIDs/GIDs to use for dynamic ID allocation."}; - #if __linux__ + #ifdef __linux__ Setting useCgroups{ this, false, "use-cgroups", R"( @@ -506,7 +501,7 @@ public: Setting keepLog{ this, true, "keep-build-log", R"( - If set to `true` (the default), Nix will write the build log of a + If set to `true` (the default), Nix writes the build log of a derivation (i.e. the standard output and error of its builder) to the directory `/nix/var/log/nix/drvs`. The build log can be retrieved using the command `nix-store -l path`. @@ -517,8 +512,8 @@ public: this, true, "compress-build-log", R"( If set to `true` (the default), build logs written to - `/nix/var/log/nix/drvs` will be compressed on the fly using bzip2. - Otherwise, they will not be compressed. + `/nix/var/log/nix/drvs` are compressed on the fly using bzip2. + Otherwise, they are not compressed. )", {"build-compress-log"}}; @@ -537,14 +532,14 @@ public: Setting gcKeepOutputs{ this, false, "keep-outputs", R"( - If `true`, the garbage collector will keep the outputs of - non-garbage derivations. If `false` (default), outputs will be + If `true`, the garbage collector keeps the outputs of + non-garbage derivations. If `false` (default), outputs are deleted unless they are GC roots themselves (or reachable from other roots). In general, outputs must be registered as roots separately. However, even if the output of a derivation is registered as a root, the - collector will still delete store paths that are used only at build + collector still deletes store paths that are used only at build time (e.g., the C compiler, or source tarballs downloaded from the network). To prevent it from doing so, set this option to `true`. )", @@ -553,9 +548,9 @@ public: Setting gcKeepDerivations{ this, true, "keep-derivations", R"( - If `true` (default), the garbage collector will keep the derivations - from which non-garbage store paths were built. If `false`, they will - be deleted unless explicitly registered as a root (or reachable from + If `true` (default), the garbage collector keeps the derivations + from which non-garbage store paths were built. If `false`, they are + deleted unless explicitly registered as a root (or reachable from other roots). Keeping derivation around is useful for querying and traceability @@ -585,7 +580,7 @@ public: If `true`, when you add a Nix derivation to a user environment, the path of the derivation is stored in the user environment. Thus, the - derivation will not be garbage-collected until the user environment + derivation isn't garbage-collected until the user environment generation is deleted (`nix-env --delete-generations`). To prevent build-time-only dependencies from being collected, you should also turn on `keep-outputs`. @@ -599,16 +594,16 @@ public: Setting sandboxMode{ this, - #if __linux__ + #ifdef __linux__ smEnabled #else smDisabled #endif , "sandbox", R"( - If set to `true`, builds will be performed in a *sandboxed + If set to `true`, builds are performed in a *sandboxed environment*, i.e., they’re isolated from the normal file system - hierarchy and will only see their dependencies in the Nix store, + hierarchy and only see their dependencies in the Nix store, the temporary build directory, private versions of `/proc`, `/dev`, `/dev/shm` and `/dev/pts` (on Linux), and the paths configured with the `sandbox-paths` option. This is useful to @@ -637,13 +632,13 @@ public: R"( A list of paths bind-mounted into Nix sandbox environments. You can use the syntax `target=source` to mount a path in a different - location in the sandbox; for instance, `/bin=/nix-bin` will mount + location in the sandbox; for instance, `/bin=/nix-bin` mounts the path `/nix-bin` as `/bin` inside the sandbox. If *source* is followed by `?`, then it is not an error if *source* does not exist; - for example, `/dev/nvidiactl?` specifies that `/dev/nvidiactl` will + for example, `/dev/nvidiactl?` specifies that `/dev/nvidiactl` only be mounted in the sandbox if it exists in the host filesystem. - If the source is in the Nix store, then its closure will be added to + If the source is in the Nix store, then its closure is added to the sandbox as well. Depending on how Nix was built, the default value for this option @@ -658,15 +653,15 @@ public: Setting requireDropSupplementaryGroups{this, isRootUser(), "require-drop-supplementary-groups", R"( Following the principle of least privilege, - Nix will attempt to drop supplementary groups when building with sandboxing. + Nix attempts to drop supplementary groups when building with sandboxing. However this can fail under some circumstances. For example, if the user lacks the `CAP_SETGID` capability. Search `setgroups(2)` for `EPERM` to find more detailed information on this. - If you encounter such a failure, setting this option to `false` will let you ignore it and continue. + If you encounter such a failure, setting this option to `false` enables you to ignore it and continue. But before doing so, you should consider the security implications carefully. - Not dropping supplementary groups means the build sandbox will be less restricted than intended. + Not dropping supplementary groups means the build sandbox is less restricted than intended. This option defaults to `true` when the user is root (since `root` usually has permissions to call setgroups) @@ -674,7 +669,7 @@ public: )"}; #endif -#if __linux__ +#ifdef __linux__ Setting sandboxShmSize{ this, "50%", "sandbox-dev-shm-size", R"( @@ -685,7 +680,9 @@ public: description of the `size` option of `tmpfs` in mount(8). The default is `50%`. )"}; +#endif +#if defined(__linux__) || defined(__FreeBSD__) Setting sandboxBuildDir{this, "/build", "sandbox-build-dir", R"( *Linux only* @@ -698,20 +695,13 @@ public: Setting> buildDir{this, std::nullopt, "build-dir", R"( - The directory on the host, in which derivations' temporary build directories are created. - - If not set, Nix will use the system temporary directory indicated by the `TMPDIR` environment variable. - Note that builds are often performed by the Nix daemon, so its `TMPDIR` is used, and not that of the Nix command line interface. - - This is also the location where [`--keep-failed`](@docroot@/command-ref/opt-common.md#opt-keep-failed) leaves its files. - - If Nix runs without sandbox, or if the platform does not support sandboxing with bind mounts (e.g. macOS), then the [`builder`](@docroot@/language/derivations.md#attr-builder)'s environment will contain this directory, instead of the virtual location [`sandbox-build-dir`](#conf-sandbox-build-dir). + Override the `build-dir` store setting for all stores that have this setting. )"}; Setting allowedImpureHostPrefixes{this, {}, "allowed-impure-host-deps", "Which prefixes to allow derivations to ask for access to (primarily for Darwin)."}; -#if __APPLE__ +#ifdef __APPLE__ Setting darwinLogSandboxViolations{this, false, "darwin-log-sandbox-violations", "Whether to log Darwin sandbox access violations to the system log."}; #endif @@ -746,12 +736,11 @@ public: 3. The path to the build's derivation - 4. The path to the build's scratch directory. This directory will - exist only if the build was run with `--keep-failed`. + 4. The path to the build's scratch directory. This directory + exists only if the build was run with `--keep-failed`. - The stderr and stdout output from the diff hook will not be - displayed to the user. Instead, it will print to the nix-daemon's - log. + The stderr and stdout output from the diff hook isn't + displayed to the user. Instead, it print to the nix-daemon's log. When using the Nix daemon, `diff-hook` must be set in the `nix.conf` configuration file, and cannot be passed at the command line. @@ -789,8 +778,8 @@ public: this, 60 * 60, "tarball-ttl", R"( The number of seconds a downloaded tarball is considered fresh. If - the cached tarball is stale, Nix will check whether it is still up - to date using the ETag header. Nix will download a new version if + the cached tarball is stale, Nix checks whether it is still up + to date using the ETag header. Nix downloads a new version if the ETag header is unsupported, or the cached ETag doesn't match. Setting the TTL to `0` forces Nix to always check if the tarball is @@ -825,7 +814,7 @@ public: R"( System types of executables that can be run on this machine. - Nix will only build a given [derivation](@docroot@/language/derivations.md) locally when its `system` attribute equals any of the values specified here or in the [`system` option](#conf-system). + Nix only builds a given [store derivation](@docroot@/glossary.md#gloss-store-derivation) locally when its `system` attribute equals any of the values specified here or in the [`system` option](#conf-system). Setting this can be useful to build derivations locally on compatible machines: - `i686-linux` executables can be run on `x86_64-linux` machines (set by default) @@ -835,7 +824,7 @@ public: - `qemu-user` may be used to support non-native platforms (though this may be slow and buggy) - Build systems will usually detect the target platform to be the current physical system and therefore produce machine code incompatible with what may be intended in the derivation. + Build systems usually detect the target platform to be the current physical system and therefore produce machine code incompatible with what may be intended in the derivation. You should design your derivation's `builder` accordingly and cross-check the results when using this option against natively-built versions of your derivation. )", {}, @@ -882,7 +871,7 @@ public: On Linux, Nix can run builds in a user namespace where they run as root (UID 0) and have 65,536 UIDs available. This is primarily useful for running containers such as `systemd-nspawn` inside a Nix build. For an example, see [`tests/systemd-nspawn/nix`][nspawn]. - [nspawn]: https://github.com/NixOS/nix/blob/67bcb99700a0da1395fa063d7c6586740b304598/tests/systemd-nspawn.nix. + [nspawn]: https://github.com/NixOS/nix/blob/67bcb99700a0da1395fa063d7c6586740b304598/tests/systemd-nspawn.nix Included by default on Linux if the [`auto-allocate-uids`](#conf-auto-allocate-uids) setting is enabled. )", @@ -925,7 +914,7 @@ public: this, 3600, "narinfo-cache-negative-ttl", R"( The TTL in seconds for negative lookups. - If a store path is queried from a [substituter](#conf-substituters) but was not found, there will be a negative lookup cached in the local disk cache database for the specified duration. + If a store path is queried from a [substituter](#conf-substituters) but was not found, a negative lookup is cached in the local disk cache database for the specified duration. Set to `0` to force updating the lookup cache. @@ -941,7 +930,7 @@ public: this, 30 * 24 * 3600, "narinfo-cache-positive-ttl", R"( The TTL in seconds for positive lookups. If a store path is queried - from a substituter, the result of the query will be cached in the + from a substituter, the result of the query is cached in the local disk cache database including some of the NAR metadata. The default TTL is a month, setting a shorter TTL for positive lookups can be useful for binary caches that have frequent garbage @@ -1027,7 +1016,7 @@ public: Setting netrcFile{ this, fmt("%s/%s", nixConfDir, "netrc"), "netrc-file", R"( - If set to an absolute path to a `netrc` file, Nix will use the HTTP + If set to an absolute path to a `netrc` file, Nix uses the HTTP authentication credentials in this file when trying to download from a remote host through HTTP or HTTPS. Defaults to `$NIX_CONF_DIR/netrc`. @@ -1053,7 +1042,7 @@ public: this, getDefaultSSLCertFile(), "ssl-cert-file", R"( The path of a file containing CA certificates used to - authenticate `https://` downloads. Nix by default will use + authenticate `https://` downloads. Nix by default uses the first of the following files that exists: 1. `/etc/ssl/certs/ca-certificates.crt` @@ -1064,9 +1053,12 @@ public: 1. `NIX_SSL_CERT_FILE` 2. `SSL_CERT_FILE` - )"}; + )", + {}, + // Don't document the machine-specific default value + false}; -#if __linux__ +#ifdef __linux__ Setting filterSyscalls{ this, true, "filter-syscalls", R"( @@ -1082,7 +1074,7 @@ public: (Linux-specific.) By default, builders on Linux cannot acquire new privileges by calling setuid/setgid programs or programs that have file capabilities. For example, programs such as `sudo` or `ping` - will fail. (Note that in sandbox builds, no such programs are + should fail. (Note that in sandbox builds, no such programs are available unless you bind-mount them into the sandbox via the `sandbox-paths` option.) You can allow the use of such programs by enabling this option. This is impure and usually undesirable, but @@ -1091,7 +1083,7 @@ public: )"}; #endif -#if HAVE_ACL_SUPPORT +#if NIX_SUPPORT_ACL Setting ignoredAcls{ this, {"security.selinux", "system.nfs4_acl", "security.csm"}, "ignored-acls", R"( @@ -1106,7 +1098,7 @@ public: this, {}, "hashed-mirrors", R"( A list of web servers used by `builtins.fetchurl` to obtain files by - hash. Given a hash algorithm *ha* and a base-16 hash *h*, Nix will try to + hash. Given a hash algorithm *ha* and a base-16 hash *h*, Nix tries to download the file from *hashed-mirror*/*ha*/*h*. This allows files to be downloaded even if they have disappeared from their original URI. For example, given an example mirror `http://tarballs.nixos.org/`, @@ -1121,7 +1113,7 @@ public: Nix will attempt to download this file from `http://tarballs.nixos.org/sha256/2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae` - first. If it is not available there, if will try the original URI. + first. If it is not available there, it tries the original URI. )"}; Setting minFree{ @@ -1153,8 +1145,8 @@ public: Setting allowSymlinkedStore{ this, false, "allow-symlinked-store", R"( - If set to `true`, Nix will stop complaining if the store directory - (typically /nix/store) contains symlink components. + If set to `true`, Nix stops complaining if the store directory + (typically `/nix/store`) contains symlink components. This risks making some builds "impure" because builders sometimes "canonicalise" paths by resolving all symlink components. Problems @@ -1166,7 +1158,7 @@ public: Setting useXDGBaseDirectories{ this, false, "use-xdg-base-directories", R"( - If set to `true`, Nix will conform to the [XDG Base Directory Specification] for files in `$HOME`. + If set to `true`, Nix conforms to the [XDG Base Directory Specification] for files in `$HOME`. The environment variables used to implement this are documented in the [Environment Variables section](@docroot@/command-ref/env-common.md). [XDG Base Directory Specification]: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html @@ -1204,7 +1196,7 @@ public: If the user is trusted (see `trusted-users` option), when building a fixed-output derivation, environment variables set in this option - will be passed to the builder if they are listed in [`impureEnvVars`](@docroot@/language/advanced-attributes.md#adv-attr-impureEnvVars). + is passed to the builder if they are listed in [`impureEnvVars`](@docroot@/language/advanced-attributes.md#adv-attr-impureEnvVars). This option is useful for, e.g., setting `https_proxy` for fixed-output derivations and in a multi-user Nix installation, or @@ -1253,7 +1245,15 @@ void loadConfFile(AbstractConfig & config); // Used by the Settings constructor std::vector getUserConfigFiles(); -extern const std::string nixVersion; +/** + * The version of Nix itself. + * + * This is not `const`, so that the Nix CLI can provide a more detailed version + * number including the git revision, without having to "re-compile" the entire + * set of Nix libraries to include that version, even when those libraries are + * not affected by the change. + */ +extern std::string nixVersion; /** * @param loadConfig Whether to load configuration from `nix.conf`, `NIX_CONFIG`, etc. May be disabled for unit tests. diff --git a/src/libstore/include/nix/store/http-binary-cache-store.hh b/src/libstore/include/nix/store/http-binary-cache-store.hh new file mode 100644 index 00000000000..66ec5f8d254 --- /dev/null +++ b/src/libstore/include/nix/store/http-binary-cache-store.hh @@ -0,0 +1,28 @@ +#include "nix/store/binary-cache-store.hh" + +namespace nix { + +struct HttpBinaryCacheStoreConfig : std::enable_shared_from_this, + virtual Store::Config, + BinaryCacheStoreConfig +{ + using BinaryCacheStoreConfig::BinaryCacheStoreConfig; + + HttpBinaryCacheStoreConfig( + std::string_view scheme, std::string_view cacheUri, const Store::Config::Params & params); + + Path cacheUri; + + static const std::string name() + { + return "HTTP Binary Cache Store"; + } + + static StringSet uriSchemes(); + + static std::string doc(); + + ref openStore() const override; +}; + +} diff --git a/src/libstore/indirect-root-store.hh b/src/libstore/include/nix/store/indirect-root-store.hh similarity index 98% rename from src/libstore/indirect-root-store.hh rename to src/libstore/include/nix/store/indirect-root-store.hh index b74ebc1eed4..bbdad83f309 100644 --- a/src/libstore/indirect-root-store.hh +++ b/src/libstore/include/nix/store/indirect-root-store.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "local-fs-store.hh" +#include "nix/store/local-fs-store.hh" namespace nix { diff --git a/src/libstore/keys.hh b/src/libstore/include/nix/store/keys.hh similarity index 64% rename from src/libstore/keys.hh rename to src/libstore/include/nix/store/keys.hh index 3da19493fbb..77aec6bb201 100644 --- a/src/libstore/keys.hh +++ b/src/libstore/include/nix/store/keys.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "signature/local-keys.hh" +#include "nix/util/signature/local-keys.hh" namespace nix { diff --git a/src/libstore/legacy-ssh-store.hh b/src/libstore/include/nix/store/legacy-ssh-store.hh similarity index 60% rename from src/libstore/legacy-ssh-store.hh rename to src/libstore/include/nix/store/legacy-ssh-store.hh index b541455b4e5..65f29d6499d 100644 --- a/src/libstore/legacy-ssh-store.hh +++ b/src/libstore/include/nix/store/legacy-ssh-store.hh @@ -1,15 +1,16 @@ #pragma once ///@file -#include "common-ssh-store-config.hh" -#include "store-api.hh" -#include "ssh.hh" -#include "callback.hh" -#include "pool.hh" +#include "nix/store/common-ssh-store-config.hh" +#include "nix/store/store-api.hh" +#include "nix/store/ssh.hh" +#include "nix/util/callback.hh" +#include "nix/util/pool.hh" +#include "nix/store/serve-protocol.hh" namespace nix { -struct LegacySSHStoreConfig : virtual CommonSSHStoreConfig +struct LegacySSHStoreConfig : std::enable_shared_from_this, virtual CommonSSHStoreConfig { using CommonSSHStoreConfig::CommonSSHStoreConfig; @@ -18,29 +19,45 @@ struct LegacySSHStoreConfig : virtual CommonSSHStoreConfig std::string_view authority, const Params & params); +#ifndef _WIN32 + // Hack for getting remote build log output. + // Intentionally not in `LegacySSHStoreConfig` so that it doesn't appear in + // the documentation + const Setting logFD{this, INVALID_DESCRIPTOR, "log-fd", "file descriptor to which SSH's stderr is connected"}; +#else + Descriptor logFD = INVALID_DESCRIPTOR; +#endif + const Setting remoteProgram{this, {"nix-store"}, "remote-program", "Path to the `nix-store` executable on the remote machine."}; const Setting maxConnections{this, 1, "max-connections", "Maximum number of concurrent SSH connections."}; - const std::string name() override { return "SSH Store"; } + /** + * Hack for hydra + */ + Strings extraSshArgs = {}; + + /** + * Exposed for hydra + */ + std::optional connPipeSize; + + static const std::string name() { return "SSH Store"; } - static std::set uriSchemes() { return {"ssh"}; } + static StringSet uriSchemes() { return {"ssh"}; } - std::string doc() override; + static std::string doc(); + + ref openStore() const override; }; -struct LegacySSHStore : public virtual LegacySSHStoreConfig, public virtual Store +struct LegacySSHStore : public virtual Store { -#ifndef _WIN32 - // Hack for getting remote build log output. - // Intentionally not in `LegacySSHStoreConfig` so that it doesn't appear in - // the documentation - const Setting logFD{this, INVALID_DESCRIPTOR, "log-fd", "file descriptor to which SSH's stderr is connected"}; -#else - Descriptor logFD = INVALID_DESCRIPTOR; -#endif + using Config = LegacySSHStoreConfig; + + ref config; struct Connection; @@ -48,10 +65,7 @@ struct LegacySSHStore : public virtual LegacySSHStoreConfig, public virtual Stor SSHMaster master; - LegacySSHStore( - std::string_view scheme, - std::string_view host, - const Params & params); + LegacySSHStore(ref); ref openConnection(); @@ -60,11 +74,24 @@ struct LegacySSHStore : public virtual LegacySSHStoreConfig, public virtual Stor void queryPathInfoUncached(const StorePath & path, Callback> callback) noexcept override; + std::map queryPathInfosUncached( + const StorePathSet & paths); + void addToStore(const ValidPathInfo & info, Source & source, RepairFlag repair, CheckSigsFlag checkSigs) override; void narFromPath(const StorePath & path, Sink & sink) override; + /** + * Hands over the connection temporarily as source to the given + * function. The function must not consume beyond the NAR; it can + * not just blindly try to always read more bytes until it is + * cut-off. + * + * This is exposed for sake of Hydra. + */ + void narFromPath(const StorePath & path, std::function fun); + std::optional queryPathFromHashPart(const std::string & hashPart) override { unsupported("queryPathFromHashPart"); } @@ -93,6 +120,16 @@ public: BuildResult buildDerivation(const StorePath & drvPath, const BasicDerivation & drv, BuildMode buildMode) override; + /** + * Note, the returned function must only be called once, or we'll + * try to read from the connection twice. + * + * @todo Use C++23 `std::move_only_function`. + */ + std::function buildDerivationAsync( + const StorePath & drvPath, const BasicDerivation & drv, + const ServeProto::BuildOptions & options); + void buildPaths(const std::vector & drvPaths, BuildMode buildMode, std::shared_ptr evalStore) override; void ensurePath(const StorePath & path) override @@ -119,18 +156,41 @@ public: StorePathSet queryValidPaths(const StorePathSet & paths, SubstituteFlag maybeSubstitute = NoSubstitute) override; + /** + * Custom variation that atomically creates temp locks on the remote + * side. + * + * This exists to prevent a race where the remote host + * garbage-collects paths that are already there. Optionally, ask + * the remote host to substitute missing paths. + */ + StorePathSet queryValidPaths(const StorePathSet & paths, + bool lock, + SubstituteFlag maybeSubstitute = NoSubstitute); + + /** + * Just exists because this is exactly what Hydra was doing, and we + * don't yet want an algorithmic change. + */ + void addMultipleToStoreLegacy(Store & srcStore, const StorePathSet & paths); + void connect() override; unsigned int getProtocol() override; + struct ConnectionStats { + size_t bytesReceived, bytesSent; + }; + + ConnectionStats getConnectionStats(); + + pid_t getConnectionPid(); + /** * The legacy ssh protocol doesn't support checking for trusted-user. * Try using ssh-ng:// instead if you want to know. */ - std::optional isTrustedClient() override - { - return std::nullopt; - } + std::optional isTrustedClient() override; void queryRealisationUncached(const DrvOutput &, Callback> callback) noexcept override diff --git a/src/libstore/length-prefixed-protocol-helper.hh b/src/libstore/include/nix/store/length-prefixed-protocol-helper.hh similarity index 89% rename from src/libstore/length-prefixed-protocol-helper.hh rename to src/libstore/include/nix/store/length-prefixed-protocol-helper.hh index 7e977bbf1a2..a83635aa4c5 100644 --- a/src/libstore/length-prefixed-protocol-helper.hh +++ b/src/libstore/include/nix/store/length-prefixed-protocol-helper.hh @@ -8,7 +8,7 @@ * Used by both the Worker and Serve protocols. */ -#include "types.hh" +#include "nix/util/types.hh" namespace nix { @@ -52,8 +52,10 @@ struct LengthPrefixedProtoHelper; template LENGTH_PREFIXED_PROTO_HELPER(Inner, std::vector); -template -LENGTH_PREFIXED_PROTO_HELPER(Inner, std::set); +#define COMMA_ , +template +LENGTH_PREFIXED_PROTO_HELPER(Inner, std::set); +#undef COMMA_ template LENGTH_PREFIXED_PROTO_HELPER(Inner, std::tuple); @@ -86,12 +88,11 @@ LengthPrefixedProtoHelper>::write( } } -template -std::set -LengthPrefixedProtoHelper>::read( +template +std::set LengthPrefixedProtoHelper>::read( const StoreDirConfig & store, typename Inner::ReadConn conn) { - std::set resSet; + std::set resSet; auto size = readNum(conn.from); while (size--) { resSet.insert(S::read(store, conn)); @@ -99,10 +100,9 @@ LengthPrefixedProtoHelper>::read( return resSet; } -template -void -LengthPrefixedProtoHelper>::write( - const StoreDirConfig & store, typename Inner::WriteConn conn, const std::set & resSet) +template +void LengthPrefixedProtoHelper>::write( + const StoreDirConfig & store, typename Inner::WriteConn conn, const std::set & resSet) { conn.to << resSet.size(); for (auto & key : resSet) { diff --git a/src/libstore/include/nix/store/local-binary-cache-store.hh b/src/libstore/include/nix/store/local-binary-cache-store.hh new file mode 100644 index 00000000000..780eaf4808e --- /dev/null +++ b/src/libstore/include/nix/store/local-binary-cache-store.hh @@ -0,0 +1,31 @@ +#include "nix/store/binary-cache-store.hh" + +namespace nix { + +struct LocalBinaryCacheStoreConfig : std::enable_shared_from_this, + virtual Store::Config, + BinaryCacheStoreConfig +{ + using BinaryCacheStoreConfig::BinaryCacheStoreConfig; + + /** + * @param binaryCacheDir `file://` is a short-hand for `file:///` + * for now. + */ + LocalBinaryCacheStoreConfig(std::string_view scheme, PathView binaryCacheDir, const Params & params); + + Path binaryCacheDir; + + static const std::string name() + { + return "Local Binary Cache Store"; + } + + static StringSet uriSchemes(); + + static std::string doc(); + + ref openStore() const override; +}; + +} diff --git a/src/libstore/local-fs-store.hh b/src/libstore/include/nix/store/local-fs-store.hh similarity index 77% rename from src/libstore/local-fs-store.hh rename to src/libstore/include/nix/store/local-fs-store.hh index 9bb569f0b25..d5fafb0c61b 100644 --- a/src/libstore/local-fs-store.hh +++ b/src/libstore/include/nix/store/local-fs-store.hh @@ -1,9 +1,9 @@ #pragma once ///@file -#include "store-api.hh" -#include "gc-store.hh" -#include "log-store.hh" +#include "nix/store/store-api.hh" +#include "nix/store/gc-store.hh" +#include "nix/store/log-store.hh" namespace nix { @@ -20,36 +20,39 @@ struct LocalFSStoreConfig : virtual StoreConfig */ LocalFSStoreConfig(PathView path, const Params & params); - const OptionalPathSetting rootDir{this, std::nullopt, + OptionalPathSetting rootDir{this, std::nullopt, "root", "Directory prefixed to all other paths."}; - const PathSetting stateDir{this, + PathSetting stateDir{this, rootDir.get() ? *rootDir.get() + "/nix/var/nix" : settings.nixStateDir, "state", - "Directory where Nix will store state."}; + "Directory where Nix stores state."}; - const PathSetting logDir{this, + PathSetting logDir{this, rootDir.get() ? *rootDir.get() + "/nix/var/log/nix" : settings.nixLogDir, "log", - "directory where Nix will store log files."}; + "directory where Nix stores log files."}; - const PathSetting realStoreDir{this, + PathSetting realStoreDir{this, rootDir.get() ? *rootDir.get() + "/nix/store" : storeDir, "real", "Physical path of the Nix store."}; }; -class LocalFSStore : public virtual LocalFSStoreConfig, - public virtual Store, - public virtual GcStore, - public virtual LogStore +struct LocalFSStore : + virtual Store, + virtual GcStore, + virtual LogStore { -public: + using Config = LocalFSStoreConfig; + + const Config & config; + inline static std::string operationName = "Local Filesystem Store"; const static std::string drvsLogDir; - LocalFSStore(const Params & params); + LocalFSStore(const Config & params); void narFromPath(const StorePath & path, Sink & sink) override; ref getFSAccessor(bool requireValidPath = true) override; @@ -70,7 +73,7 @@ public: */ virtual Path addPermRoot(const StorePath & storePath, const Path & gcRoot) = 0; - virtual Path getRealStoreDir() { return realStoreDir; } + virtual Path getRealStoreDir() { return config.realStoreDir; } Path toRealPath(const Path & storePath) override { diff --git a/src/libstore/local-overlay-store.hh b/src/libstore/include/nix/store/local-overlay-store.hh similarity index 91% rename from src/libstore/local-overlay-store.hh rename to src/libstore/include/nix/store/local-overlay-store.hh index 63628abed50..6077d9e535c 100644 --- a/src/libstore/local-overlay-store.hh +++ b/src/libstore/include/nix/store/local-overlay-store.hh @@ -1,4 +1,4 @@ -#include "local-store.hh" +#include "nix/store/local-store.hh" namespace nix { @@ -56,19 +56,21 @@ struct LocalOverlayStoreConfig : virtual LocalStoreConfig The store directory is passed as an argument to the invoked executable. )"}; - const std::string name() override { return "Experimental Local Overlay Store"; } + static const std::string name() { return "Experimental Local Overlay Store"; } - std::optional experimentalFeature() const override + static std::optional experimentalFeature() { return ExperimentalFeature::LocalOverlayStore; } - static std::set uriSchemes() + static StringSet uriSchemes() { return { "local-overlay" }; } - std::string doc() override; + static std::string doc(); + + ref openStore() const override; protected: /** @@ -79,7 +81,9 @@ protected: * at that file path. It might be stored in the lower layer instead, * or it might not be part of this store at all. */ - Path toUpperPath(const StorePath & path); + Path toUpperPath(const StorePath & path) const; + + friend struct LocalOverlayStore; }; /** @@ -88,24 +92,13 @@ protected: * Documentation on overridden methods states how they differ from their * `LocalStore` counterparts. */ -class LocalOverlayStore : public virtual LocalOverlayStoreConfig, public virtual LocalStore +struct LocalOverlayStore : virtual LocalStore { - /** - * The store beneath us. - * - * Our store dir should be an overlay fs where the lower layer - * is that store's store dir, and the upper layer is some - * scratch storage just for us. - */ - ref lowerStore; + using Config = LocalOverlayStoreConfig; -public: - LocalOverlayStore(const Params & params) - : LocalOverlayStore("local-overlay", "", params) - { - } + ref config; - LocalOverlayStore(std::string_view scheme, PathView path, const Params & params); + LocalOverlayStore(ref); std::string getUri() override { @@ -113,6 +106,15 @@ public: } private: + /** + * The store beneath us. + * + * Our store dir should be an overlay fs where the lower layer + * is that store's store dir, and the upper layer is some + * scratch storage just for us. + */ + ref lowerStore; + /** * First copy up any lower store realisation with the same key, so we * merge rather than mask it. diff --git a/src/libstore/local-store.hh b/src/libstore/include/nix/store/local-store.hh similarity index 80% rename from src/libstore/local-store.hh rename to src/libstore/include/nix/store/local-store.hh index 83154d65193..abaf1b981a0 100644 --- a/src/libstore/local-store.hh +++ b/src/libstore/include/nix/store/local-store.hh @@ -1,12 +1,12 @@ #pragma once ///@file -#include "sqlite.hh" +#include "nix/store/sqlite.hh" -#include "pathlocks.hh" -#include "store-api.hh" -#include "indirect-root-store.hh" -#include "sync.hh" +#include "nix/store/pathlocks.hh" +#include "nix/store/store-api.hh" +#include "nix/store/indirect-root-store.hh" +#include "nix/util/sync.hh" #include #include @@ -34,7 +34,39 @@ struct OptimiseStats uint64_t bytesFreed = 0; }; -struct LocalStoreConfig : virtual LocalFSStoreConfig +struct LocalBuildStoreConfig : virtual LocalFSStoreConfig +{ + +private: + /** + Input for computing the build directory. See `getBuildDir()`. + */ + Setting> buildDir{this, std::nullopt, "build-dir", + R"( + The directory on the host, in which derivations' temporary build directories are created. + + If not set, Nix will use the `builds` subdirectory of its configured state directory. + + Note that builds are often performed by the Nix daemon, so its `build-dir` applies. + + Nix will create this directory automatically with suitable permissions if it does not exist. + Otherwise its permissions must allow all users to traverse the directory (i.e. it must have `o+x` set, in unix parlance) for non-sandboxed builds to work correctly. + + This is also the location where [`--keep-failed`](@docroot@/command-ref/opt-common.md#opt-keep-failed) leaves its files. + + If Nix runs without sandbox, or if the platform does not support sandboxing with bind mounts (e.g. macOS), then the [`builder`](@docroot@/language/derivations.md#attr-builder)'s environment will contain this directory, instead of the virtual location [`sandbox-build-dir`](#conf-sandbox-build-dir). + + > **Warning** + > + > `build-dir` must not be set to a world-writable directory. + > Placing temporary build directories in a world-writable place allows other users to access or modify build data that is currently in use. + > This alone is merely an impurity, but combined with another factor this has allowed malicious derivations to escape the build sandbox. + )"}; +public: + Path getBuildDir() const; +}; + +struct LocalStoreConfig : std::enable_shared_from_this, virtual LocalFSStoreConfig, virtual LocalBuildStoreConfig { using LocalFSStoreConfig::LocalFSStoreConfig; @@ -54,7 +86,7 @@ struct LocalStoreConfig : virtual LocalFSStoreConfig R"( Allow this store to be opened when its [database](@docroot@/glossary.md#gloss-nix-database) is on a read-only filesystem. - Normally Nix will attempt to open the store database in read-write mode, even for querying (when write access is not needed), causing it to fail if the database is on a read-only filesystem. + Normally Nix attempts to open the store database in read-write mode, even for querying (when write access is not needed), causing it to fail if the database is on a read-only filesystem. Enable read-only mode to disable locking and open the SQLite database with the [`immutable` parameter](https://www.sqlite.org/c3ref/open.html) set. @@ -65,18 +97,26 @@ struct LocalStoreConfig : virtual LocalFSStoreConfig > While the filesystem the database resides on might appear to be read-only, consider whether another user or system might have write access to it. )"}; - const std::string name() override { return "Local Store"; } + static const std::string name() { return "Local Store"; } - static std::set uriSchemes() + static StringSet uriSchemes() { return {"local"}; } - std::string doc() override; + static std::string doc(); + + ref openStore() const override; }; -class LocalStore : public virtual LocalStoreConfig - , public virtual IndirectRootStore - , public virtual GcStore +class LocalStore : + public virtual IndirectRootStore, + public virtual GcStore { +public: + + using Config = LocalStoreConfig; + + ref config; + private: /** @@ -144,11 +184,7 @@ public: * Initialise the local store, upgrading the schema if * necessary. */ - LocalStore(const Params & params); - LocalStore( - std::string_view scheme, - PathView path, - const Params & params); + LocalStore(ref params); ~LocalStore(); @@ -396,18 +432,9 @@ private: bool isValidPath_(State & state, const StorePath & path); void queryReferrers(State & state, const StorePath & path, StorePathSet & referrers); - /** - * Add signatures to a ValidPathInfo or Realisation using the secret keys - * specified by the ‘secret-key-files’ option. - */ - void signPathInfo(ValidPathInfo & info); - void signRealisation(Realisation &); - void addBuildLog(const StorePath & drvPath, std::string_view log) override; - friend struct LocalDerivationGoal; friend struct PathSubstitutionGoal; - friend struct SubstitutionGoal; friend struct DerivationGoal; }; diff --git a/src/libstore/log-store.hh b/src/libstore/include/nix/store/log-store.hh similarity index 94% rename from src/libstore/log-store.hh rename to src/libstore/include/nix/store/log-store.hh index a84f7dbeb25..fc12b0c479a 100644 --- a/src/libstore/log-store.hh +++ b/src/libstore/include/nix/store/log-store.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "store-api.hh" +#include "nix/store/store-api.hh" namespace nix { diff --git a/src/libstore/machines.hh b/src/libstore/include/nix/store/machines.hh similarity index 80% rename from src/libstore/machines.hh rename to src/libstore/include/nix/store/machines.hh index b70ab907806..2bf7408f624 100644 --- a/src/libstore/machines.hh +++ b/src/libstore/include/nix/store/machines.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "ref.hh" -#include "store-reference.hh" +#include "nix/util/ref.hh" +#include "nix/store/store-reference.hh" namespace nix { @@ -15,12 +15,12 @@ typedef std::vector Machines; struct Machine { const StoreReference storeUri; - const std::set systemTypes; + const StringSet systemTypes; const std::string sshKey; const unsigned int maxJobs; const float speedFactor; - const std::set supportedFeatures; - const std::set mandatoryFeatures; + const StringSet supportedFeatures; + const StringSet mandatoryFeatures; const std::string sshPublicHostKey; bool enabled = true; @@ -34,12 +34,12 @@ struct Machine { * @return Whether `features` is a subset of the union of `supportedFeatures` and * `mandatoryFeatures`. */ - bool allSupported(const std::set & features) const; + bool allSupported(const StringSet & features) const; /** * @return Whether `mandatoryFeatures` is a subset of `features`. */ - bool mandatoryMet(const std::set & features) const; + bool mandatoryMet(const StringSet & features) const; Machine( const std::string & storeUri, @@ -75,7 +75,7 @@ struct Machine { * with `@` are interpreted as paths to other configuration files in * the same format. */ - static Machines parseConfig(const std::set & defaultSystems, const std::string & config); + static Machines parseConfig(const StringSet & defaultSystems, const std::string & config); }; /** diff --git a/src/libstore/make-content-addressed.hh b/src/libstore/include/nix/store/make-content-addressed.hh similarity index 93% rename from src/libstore/make-content-addressed.hh rename to src/libstore/include/nix/store/make-content-addressed.hh index 60bb2b477db..3881b6d40c2 100644 --- a/src/libstore/make-content-addressed.hh +++ b/src/libstore/include/nix/store/make-content-addressed.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "store-api.hh" +#include "nix/store/store-api.hh" namespace nix { diff --git a/src/libstore/include/nix/store/meson.build b/src/libstore/include/nix/store/meson.build new file mode 100644 index 00000000000..44d9815debb --- /dev/null +++ b/src/libstore/include/nix/store/meson.build @@ -0,0 +1,87 @@ +# Public headers directory + +include_dirs = [ + include_directories('../..'), +] + +config_pub_h = configure_file( + configuration : configdata_pub, + output : 'config.hh', +) + +headers = [config_pub_h] + files( + 'binary-cache-store.hh', + 'build-result.hh', + 'build/derivation-goal.hh', + 'build/derivation-building-goal.hh', + 'build/derivation-building-misc.hh', + 'build/derivation-trampoline-goal.hh', + 'build/drv-output-substitution-goal.hh', + 'build/goal.hh', + 'build/substitution-goal.hh', + 'build/worker.hh', + 'builtins.hh', + 'builtins/buildenv.hh', + 'common-protocol-impl.hh', + 'common-protocol.hh', + 'common-ssh-store-config.hh', + 'content-address.hh', + 'daemon.hh', + 'derivations.hh', + 'derivation-options.hh', + 'derived-path-map.hh', + 'derived-path.hh', + 'downstream-placeholder.hh', + 'filetransfer.hh', + 'gc-store.hh', + 'globals.hh', + 'http-binary-cache-store.hh', + 'indirect-root-store.hh', + 'keys.hh', + 'legacy-ssh-store.hh', + 'length-prefixed-protocol-helper.hh', + 'local-binary-cache-store.hh', + 'local-fs-store.hh', + 'local-overlay-store.hh', + 'local-store.hh', + 'log-store.hh', + 'machines.hh', + 'make-content-addressed.hh', + 'names.hh', + 'nar-accessor.hh', + 'nar-info-disk-cache.hh', + 'nar-info.hh', + 'outputs-spec.hh', + 'parsed-derivations.hh', + 'path-info.hh', + 'path-references.hh', + 'path-regex.hh', + 'path-with-outputs.hh', + 'path.hh', + 'pathlocks.hh', + 'posix-fs-canonicalise.hh', + 'profiles.hh', + 'realisation.hh', + 'remote-fs-accessor.hh', + 'remote-store-connection.hh', + 'remote-store.hh', + 'restricted-store.hh', + 's3-binary-cache-store.hh', + 's3.hh', + 'serve-protocol-connection.hh', + 'serve-protocol-impl.hh', + 'serve-protocol.hh', + 'sqlite.hh', + 'ssh-store.hh', + 'ssh.hh', + 'store-api.hh', + 'store-cast.hh', + 'store-dir-config.hh', + 'store-open.hh', + 'store-reference.hh', + 'store-registration.hh', + 'uds-remote-store.hh', + 'worker-protocol-connection.hh', + 'worker-protocol-impl.hh', + 'worker-protocol.hh', +) diff --git a/src/libstore/names.hh b/src/libstore/include/nix/store/names.hh similarity index 95% rename from src/libstore/names.hh rename to src/libstore/include/nix/store/names.hh index a6909d54593..ab315de6398 100644 --- a/src/libstore/names.hh +++ b/src/libstore/include/nix/store/names.hh @@ -3,7 +3,7 @@ #include -#include "types.hh" +#include "nix/util/types.hh" namespace nix { diff --git a/src/libstore/nar-accessor.hh b/src/libstore/include/nix/store/nar-accessor.hh similarity index 95% rename from src/libstore/nar-accessor.hh rename to src/libstore/include/nix/store/nar-accessor.hh index 0043897c658..199d525cbf3 100644 --- a/src/libstore/nar-accessor.hh +++ b/src/libstore/include/nix/store/nar-accessor.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "source-accessor.hh" +#include "nix/util/source-accessor.hh" #include diff --git a/src/libstore/nar-info-disk-cache.hh b/src/libstore/include/nix/store/nar-info-disk-cache.hh similarity index 93% rename from src/libstore/nar-info-disk-cache.hh rename to src/libstore/include/nix/store/nar-info-disk-cache.hh index bbd1d05d5c5..a7fde1fbf9d 100644 --- a/src/libstore/nar-info-disk-cache.hh +++ b/src/libstore/include/nix/store/nar-info-disk-cache.hh @@ -1,9 +1,9 @@ #pragma once ///@file -#include "ref.hh" -#include "nar-info.hh" -#include "realisation.hh" +#include "nix/util/ref.hh" +#include "nix/store/nar-info.hh" +#include "nix/store/realisation.hh" namespace nix { diff --git a/src/libstore/nar-info.hh b/src/libstore/include/nix/store/nar-info.hh similarity index 92% rename from src/libstore/nar-info.hh rename to src/libstore/include/nix/store/nar-info.hh index 561c9a86364..d66b6e05838 100644 --- a/src/libstore/nar-info.hh +++ b/src/libstore/include/nix/store/nar-info.hh @@ -1,9 +1,9 @@ #pragma once ///@file -#include "types.hh" -#include "hash.hh" -#include "path-info.hh" +#include "nix/util/types.hh" +#include "nix/util/hash.hh" +#include "nix/store/path-info.hh" namespace nix { diff --git a/src/libstore/outputs-spec.hh b/src/libstore/include/nix/store/outputs-spec.hh similarity index 87% rename from src/libstore/outputs-spec.hh rename to src/libstore/include/nix/store/outputs-spec.hh index 30d15311d0a..4e874a6f116 100644 --- a/src/libstore/outputs-spec.hh +++ b/src/libstore/include/nix/store/outputs-spec.hh @@ -6,20 +6,20 @@ #include #include -#include "json-impls.hh" -#include "variant-wrapper.hh" +#include "nix/util/json-impls.hh" +#include "nix/util/variant-wrapper.hh" namespace nix { /** * An (owned) output name. Just a type alias used to make code more - * readible. + * readable. */ typedef std::string OutputName; /** * A borrowed output name. Just a type alias used to make code more - * readible. + * readable. */ typedef std::string_view OutputNameView; @@ -27,20 +27,24 @@ struct OutputsSpec { /** * A non-empty set of outputs, specified by name */ - struct Names : std::set { - using std::set::set; + struct Names : std::set> { + private: + using BaseType = std::set>; + + public: + using BaseType::BaseType; /* These need to be "inherited manually" */ - Names(const std::set & s) - : std::set(s) + Names(const BaseType & s) + : BaseType(s) { assert(!empty()); } /** * Needs to be "inherited manually" */ - Names(std::set && s) - : std::set(s) + Names(BaseType && s) + : BaseType(std::move(s)) { assert(!empty()); } /* This set should always be non-empty, so we delete this diff --git a/src/libstore/include/nix/store/parsed-derivations.hh b/src/libstore/include/nix/store/parsed-derivations.hh new file mode 100644 index 00000000000..a7c053a8f8a --- /dev/null +++ b/src/libstore/include/nix/store/parsed-derivations.hh @@ -0,0 +1,43 @@ +#pragma once +///@file + +#include + +#include "nix/util/types.hh" +#include "nix/store/path.hh" + +namespace nix { + +class Store; +struct DerivationOptions; +struct DerivationOutput; + +typedef std::map DerivationOutputs; + +struct StructuredAttrs +{ + nlohmann::json structuredAttrs; + + static std::optional tryParse(const StringPairs & env); + + nlohmann::json prepareStructuredAttrs( + Store & store, + const DerivationOptions & drvOptions, + const StorePathSet & inputPaths, + const DerivationOutputs & outputs) const; + + /** + * As a convenience to bash scripts, write a shell file that + * maps all attributes that are representable in bash - + * namely, strings, integers, nulls, Booleans, and arrays and + * objects consisting entirely of those values. (So nested + * arrays or objects are not supported.) + * + * @param prepared This should be the result of + * `prepareStructuredAttrs`, *not* the original `structuredAttrs` + * field. + */ + static std::string writeShell(const nlohmann::json & prepared); +}; + +} diff --git a/src/libstore/path-info.hh b/src/libstore/include/nix/store/path-info.hh similarity index 95% rename from src/libstore/path-info.hh rename to src/libstore/include/nix/store/path-info.hh index 9a4c466a898..690f0f8134a 100644 --- a/src/libstore/path-info.hh +++ b/src/libstore/include/nix/store/path-info.hh @@ -1,10 +1,10 @@ #pragma once ///@file -#include "signature/signer.hh" -#include "path.hh" -#include "hash.hh" -#include "content-address.hh" +#include "nix/util/signature/signer.hh" +#include "nix/store/path.hh" +#include "nix/util/hash.hh" +#include "nix/store/content-address.hh" #include #include @@ -51,7 +51,7 @@ struct UnkeyedValidPathInfo Hash narHash; /** - * Other store objects this store object referes to. + * Other store objects this store object refers to. */ StorePathSet references; @@ -144,6 +144,7 @@ struct ValidPathInfo : UnkeyedValidPathInfo { std::string fingerprint(const Store & store) const; void sign(const Store & store, const Signer & signer); + void sign(const Store & store, const std::vector> & signers); /** * @return The `ContentAddressWithReferences` that determines the diff --git a/src/libstore/path-references.hh b/src/libstore/include/nix/store/path-references.hh similarity index 89% rename from src/libstore/path-references.hh rename to src/libstore/include/nix/store/path-references.hh index 0553003f83a..b8d0b4dd0f7 100644 --- a/src/libstore/path-references.hh +++ b/src/libstore/include/nix/store/path-references.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "references.hh" -#include "path.hh" +#include "nix/util/references.hh" +#include "nix/store/path.hh" namespace nix { diff --git a/src/libstore/path-regex.hh b/src/libstore/include/nix/store/path-regex.hh similarity index 94% rename from src/libstore/path-regex.hh rename to src/libstore/include/nix/store/path-regex.hh index 56c2cfc1db5..e34a305c5f9 100644 --- a/src/libstore/path-regex.hh +++ b/src/libstore/include/nix/store/path-regex.hh @@ -1,6 +1,8 @@ #pragma once ///@file +#include + namespace nix { diff --git a/src/libstore/path-with-outputs.hh b/src/libstore/include/nix/store/path-with-outputs.hh similarity index 93% rename from src/libstore/path-with-outputs.hh rename to src/libstore/include/nix/store/path-with-outputs.hh index 5f76a583a9a..368667c47c2 100644 --- a/src/libstore/path-with-outputs.hh +++ b/src/libstore/include/nix/store/path-with-outputs.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "path.hh" -#include "derived-path.hh" +#include "nix/store/path.hh" +#include "nix/store/derived-path.hh" namespace nix { @@ -19,7 +19,7 @@ struct StoreDirConfig; struct StorePathWithOutputs { StorePath path; - std::set outputs; + StringSet outputs; std::string to_string(const StoreDirConfig & store) const; diff --git a/src/libstore/path.hh b/src/libstore/include/nix/store/path.hh similarity index 98% rename from src/libstore/path.hh rename to src/libstore/include/nix/store/path.hh index 90226236258..279e9dba4fa 100644 --- a/src/libstore/path.hh +++ b/src/libstore/include/nix/store/path.hh @@ -3,7 +3,7 @@ #include -#include "types.hh" +#include "nix/util/types.hh" namespace nix { diff --git a/src/libstore/pathlocks.hh b/src/libstore/include/nix/store/pathlocks.hh similarity index 96% rename from src/libstore/pathlocks.hh rename to src/libstore/include/nix/store/pathlocks.hh index 42a84a1a37b..33cad786865 100644 --- a/src/libstore/pathlocks.hh +++ b/src/libstore/include/nix/store/pathlocks.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "file-descriptor.hh" +#include "nix/util/file-descriptor.hh" namespace nix { diff --git a/src/libstore/posix-fs-canonicalise.hh b/src/libstore/include/nix/store/posix-fs-canonicalise.hh similarity index 95% rename from src/libstore/posix-fs-canonicalise.hh rename to src/libstore/include/nix/store/posix-fs-canonicalise.hh index 45a4f3f2069..1d669602375 100644 --- a/src/libstore/posix-fs-canonicalise.hh +++ b/src/libstore/include/nix/store/posix-fs-canonicalise.hh @@ -4,8 +4,8 @@ #include #include -#include "types.hh" -#include "error.hh" +#include "nix/util/types.hh" +#include "nix/util/error.hh" namespace nix { diff --git a/src/libstore/profiles.hh b/src/libstore/include/nix/store/profiles.hh similarity index 98% rename from src/libstore/profiles.hh rename to src/libstore/include/nix/store/profiles.hh index 33fcf04b3a8..e20e1198e51 100644 --- a/src/libstore/profiles.hh +++ b/src/libstore/include/nix/store/profiles.hh @@ -7,8 +7,8 @@ * See the manual for additional information. */ -#include "types.hh" -#include "pathlocks.hh" +#include "nix/util/types.hh" +#include "nix/store/pathlocks.hh" #include #include @@ -86,7 +86,7 @@ typedef std::list Generations; */ std::pair> findGenerations(Path profile); -class LocalFSStore; +struct LocalFSStore; /** * Create a new generation of the given profile diff --git a/src/libstore/realisation.hh b/src/libstore/include/nix/store/realisation.hh similarity index 96% rename from src/libstore/realisation.hh rename to src/libstore/include/nix/store/realisation.hh index ddb4af770a2..b93ae37b652 100644 --- a/src/libstore/realisation.hh +++ b/src/libstore/include/nix/store/realisation.hh @@ -3,12 +3,12 @@ #include -#include "hash.hh" -#include "path.hh" -#include "derived-path.hh" +#include "nix/util/hash.hh" +#include "nix/store/path.hh" +#include "nix/store/derived-path.hh" #include -#include "comparator.hh" -#include "signature/signer.hh" +#include "nix/util/comparator.hh" +#include "nix/util/signature/signer.hh" namespace nix { diff --git a/src/libstore/remote-fs-accessor.hh b/src/libstore/include/nix/store/remote-fs-accessor.hh similarity index 86% rename from src/libstore/remote-fs-accessor.hh rename to src/libstore/include/nix/store/remote-fs-accessor.hh index d09762a53c4..75bb40dfb36 100644 --- a/src/libstore/remote-fs-accessor.hh +++ b/src/libstore/include/nix/store/remote-fs-accessor.hh @@ -1,9 +1,9 @@ #pragma once ///@file -#include "source-accessor.hh" -#include "ref.hh" -#include "store-api.hh" +#include "nix/util/source-accessor.hh" +#include "nix/util/ref.hh" +#include "nix/store/store-api.hh" namespace nix { @@ -19,7 +19,7 @@ class RemoteFSAccessor : public SourceAccessor std::pair, CanonPath> fetch(const CanonPath & path); - friend class BinaryCacheStore; + friend struct BinaryCacheStore; Path makeCacheFile(std::string_view hashPart, const std::string & ext); diff --git a/src/libstore/remote-store-connection.hh b/src/libstore/include/nix/store/remote-store-connection.hh similarity index 90% rename from src/libstore/remote-store-connection.hh rename to src/libstore/include/nix/store/remote-store-connection.hh index f8549d0b245..33ec265c2ac 100644 --- a/src/libstore/remote-store-connection.hh +++ b/src/libstore/include/nix/store/remote-store-connection.hh @@ -1,10 +1,10 @@ #pragma once ///@file -#include "remote-store.hh" -#include "worker-protocol.hh" -#include "worker-protocol-connection.hh" -#include "pool.hh" +#include "nix/store/remote-store.hh" +#include "nix/store/worker-protocol.hh" +#include "nix/store/worker-protocol-connection.hh" +#include "nix/util/pool.hh" namespace nix { diff --git a/src/libstore/remote-store.hh b/src/libstore/include/nix/store/remote-store.hh similarity index 92% rename from src/libstore/remote-store.hh rename to src/libstore/include/nix/store/remote-store.hh index 4e18962683d..18c02456f4c 100644 --- a/src/libstore/remote-store.hh +++ b/src/libstore/include/nix/store/remote-store.hh @@ -4,9 +4,9 @@ #include #include -#include "store-api.hh" -#include "gc-store.hh" -#include "log-store.hh" +#include "nix/store/store-api.hh" +#include "nix/store/gc-store.hh" +#include "nix/store/log-store.hh" namespace nix { @@ -35,14 +35,16 @@ struct RemoteStoreConfig : virtual StoreConfig * \todo RemoteStore is a misnomer - should be something like * DaemonStore. */ -class RemoteStore : public virtual RemoteStoreConfig, +struct RemoteStore : public virtual Store, public virtual GcStore, public virtual LogStore { -public: + using Config = RemoteStoreConfig; - RemoteStore(const Params & params); + const Config & config; + + RemoteStore(const Config & config); /* Implementations of abstract store API methods. */ @@ -102,7 +104,7 @@ public: CheckSigsFlag checkSigs) override; void addMultipleToStore( - PathsSource & pathsToCopy, + PathsSource && pathsToCopy, Activity & act, RepairFlag repair, CheckSigsFlag checkSigs) override; @@ -147,9 +149,7 @@ public: void addSignatures(const StorePath & storePath, const StringSet & sigs) override; - void queryMissing(const std::vector & targets, - StorePathSet & willBuild, StorePathSet & willSubstitute, StorePathSet & unknown, - uint64_t & downloadSize, uint64_t & narSize) override; + MissingPaths queryMissing(const std::vector & targets) override; void addBuildLog(const StorePath & drvPath, std::string_view log) override; diff --git a/src/libstore/include/nix/store/restricted-store.hh b/src/libstore/include/nix/store/restricted-store.hh new file mode 100644 index 00000000000..6f2122c7b58 --- /dev/null +++ b/src/libstore/include/nix/store/restricted-store.hh @@ -0,0 +1,60 @@ +#pragma once +///@file + +#include "nix/store/local-store.hh" + +namespace nix { + +/** + * A restricted store has a pointer to one of these, which manages the + * restrictions that are in place. + * + * This is a separate data type so the whitelists can be mutated before + * the restricted store is created: put differently, someones we don't + * know whether we will in fact create a restricted store, but we need + * to prepare the whitelists just in case. + * + * It is possible there are other ways to solve this problem. This was + * just the easiest place to begin, when this was extracted from + * `LocalDerivationGoal`. + */ +struct RestrictionContext +{ + /** + * Paths that are already allowed to begin with + */ + virtual const StorePathSet & originalPaths() = 0; + + /** + * Paths that were added via recursive Nix calls. + */ + StorePathSet addedPaths; + + /** + * Realisations that were added via recursive Nix calls. + */ + std::set addedDrvOutputs; + + /** + * Recursive Nix calls are only allowed to build or realize paths + * in the original input closure or added via a recursive Nix call + * (so e.g. you can't do 'nix-store -r /nix/store/' where + * /nix/store/ is some arbitrary path in a binary cache). + */ + virtual bool isAllowed(const StorePath &) = 0; + virtual bool isAllowed(const DrvOutput & id) = 0; + bool isAllowed(const DerivedPath & id); + + /** + * Add 'path' to the set of paths that may be referenced by the + * outputs, and make it appear in the sandbox. + */ + virtual void addDependency(const StorePath & path) = 0; +}; + +/** + * Create a shared pointer to a restricted store. + */ +ref makeRestrictedStore(ref config, ref next, RestrictionContext & context); + +} diff --git a/src/libstore/s3-binary-cache-store.hh b/src/libstore/include/nix/store/s3-binary-cache-store.hh similarity index 78% rename from src/libstore/s3-binary-cache-store.hh rename to src/libstore/include/nix/store/s3-binary-cache-store.hh index 7d303a115f4..c38591e60f3 100644 --- a/src/libstore/s3-binary-cache-store.hh +++ b/src/libstore/include/nix/store/s3-binary-cache-store.hh @@ -1,13 +1,17 @@ #pragma once ///@file -#include "binary-cache-store.hh" +#include "nix/store/config.hh" -#include +#if NIX_WITH_S3_SUPPORT + +# include "nix/store/binary-cache-store.hh" + +# include namespace nix { -struct S3BinaryCacheStoreConfig : virtual BinaryCacheStoreConfig +struct S3BinaryCacheStoreConfig : std::enable_shared_from_this, virtual BinaryCacheStoreConfig { std::string bucketName; @@ -21,7 +25,7 @@ struct S3BinaryCacheStoreConfig : virtual BinaryCacheStoreConfig "profile", R"( The name of the AWS configuration profile to use. By default - Nix will use the `default` profile. + Nix uses the `default` profile. )"}; protected: @@ -36,7 +40,7 @@ public: "region", R"( The region of the S3 bucket. If your bucket is not in - `us–east-1`, you should always explicitly specify the region + `us-east-1`, you should always explicitly specify the region parameter. )"}; @@ -65,7 +69,7 @@ public: > **Note** > - > This endpoint must support HTTPS and will use path-based + > This endpoint must support HTTPS and uses path-based > addressing instead of virtual host based addressing. )"}; @@ -89,26 +93,28 @@ public: const Setting bufferSize{ this, 5 * 1024 * 1024, "buffer-size", "Size (in bytes) of each part in multi-part uploads."}; - const std::string name() override + static const std::string name() { return "S3 Binary Cache Store"; } - static std::set uriSchemes() + static StringSet uriSchemes() { return {"s3"}; } - std::string doc() override; + static std::string doc(); + + ref openStore() const override; }; -class S3BinaryCacheStore : public virtual BinaryCacheStore +struct S3BinaryCacheStore : virtual BinaryCacheStore { -protected: + using Config = S3BinaryCacheStoreConfig; - S3BinaryCacheStore(const Params & params); + ref config; -public: + S3BinaryCacheStore(ref); struct Stats { @@ -125,3 +131,5 @@ public: }; } + +#endif diff --git a/src/libstore/s3.hh b/src/libstore/include/nix/store/s3.hh similarity index 90% rename from src/libstore/s3.hh rename to src/libstore/include/nix/store/s3.hh index 18de115aeb1..e017b7c6b5a 100644 --- a/src/libstore/s3.hh +++ b/src/libstore/include/nix/store/s3.hh @@ -1,9 +1,9 @@ #pragma once ///@file +#include "nix/store/config.hh" +#if NIX_WITH_S3_SUPPORT -#if ENABLE_S3 - -#include "ref.hh" +#include "nix/util/ref.hh" #include #include diff --git a/src/libstore/serve-protocol-connection.hh b/src/libstore/include/nix/store/serve-protocol-connection.hh similarity index 97% rename from src/libstore/serve-protocol-connection.hh rename to src/libstore/include/nix/store/serve-protocol-connection.hh index 73bf714439e..5822b499099 100644 --- a/src/libstore/serve-protocol-connection.hh +++ b/src/libstore/include/nix/store/serve-protocol-connection.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "serve-protocol.hh" -#include "store-api.hh" +#include "nix/store/serve-protocol.hh" +#include "nix/store/store-api.hh" namespace nix { diff --git a/src/libstore/serve-protocol-impl.hh b/src/libstore/include/nix/store/serve-protocol-impl.hh similarity index 84% rename from src/libstore/serve-protocol-impl.hh rename to src/libstore/include/nix/store/serve-protocol-impl.hh index 099eade648e..4e66ca542ce 100644 --- a/src/libstore/serve-protocol-impl.hh +++ b/src/libstore/include/nix/store/serve-protocol-impl.hh @@ -4,12 +4,12 @@ * * Template implementations (as opposed to mere declarations). * - * This file is an exmample of the "impl.hh" pattern. See the + * This file is an example of the "impl.hh" pattern. See the * contributing guide. */ -#include "serve-protocol.hh" -#include "length-prefixed-protocol-helper.hh" +#include "nix/store/serve-protocol.hh" +#include "nix/store/length-prefixed-protocol-helper.hh" namespace nix { @@ -26,7 +26,9 @@ namespace nix { } SERVE_USE_LENGTH_PREFIX_SERIALISER(template, std::vector) -SERVE_USE_LENGTH_PREFIX_SERIALISER(template, std::set) +#define COMMA_ , +SERVE_USE_LENGTH_PREFIX_SERIALISER(template, std::set) +#undef COMMA_ SERVE_USE_LENGTH_PREFIX_SERIALISER(template, std::tuple) #define SERVE_USE_LENGTH_PREFIX_SERIALISER_COMMA , diff --git a/src/libstore/serve-protocol.hh b/src/libstore/include/nix/store/serve-protocol.hh similarity index 97% rename from src/libstore/serve-protocol.hh rename to src/libstore/include/nix/store/serve-protocol.hh index 8c112bb74c7..6f6bf6b609a 100644 --- a/src/libstore/serve-protocol.hh +++ b/src/libstore/include/nix/store/serve-protocol.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "common-protocol.hh" +#include "nix/store/common-protocol.hh" namespace nix { @@ -180,12 +180,12 @@ DECLARE_SERVE_SERIALISER(ServeProto::BuildOptions); template DECLARE_SERVE_SERIALISER(std::vector); -template -DECLARE_SERVE_SERIALISER(std::set); +#define COMMA_ , +template +DECLARE_SERVE_SERIALISER(std::set); template DECLARE_SERVE_SERIALISER(std::tuple); -#define COMMA_ , template DECLARE_SERVE_SERIALISER(std::map); #undef COMMA_ diff --git a/src/libstore/sqlite.hh b/src/libstore/include/nix/store/sqlite.hh similarity index 99% rename from src/libstore/sqlite.hh rename to src/libstore/include/nix/store/sqlite.hh index 037380b7109..266930d75a8 100644 --- a/src/libstore/sqlite.hh +++ b/src/libstore/include/nix/store/sqlite.hh @@ -4,7 +4,7 @@ #include #include -#include "error.hh" +#include "nix/util/error.hh" struct sqlite3; struct sqlite3_stmt; diff --git a/src/libstore/ssh-store.hh b/src/libstore/include/nix/store/ssh-store.hh similarity index 57% rename from src/libstore/ssh-store.hh rename to src/libstore/include/nix/store/ssh-store.hh index 29a2a8b2c2d..fde165445fa 100644 --- a/src/libstore/ssh-store.hh +++ b/src/libstore/include/nix/store/ssh-store.hh @@ -1,14 +1,16 @@ #pragma once ///@file -#include "common-ssh-store-config.hh" -#include "store-api.hh" -#include "local-fs-store.hh" -#include "remote-store.hh" +#include "nix/store/common-ssh-store-config.hh" +#include "nix/store/store-api.hh" +#include "nix/store/local-fs-store.hh" +#include "nix/store/remote-store.hh" namespace nix { -struct SSHStoreConfig : virtual RemoteStoreConfig, virtual CommonSSHStoreConfig +struct SSHStoreConfig : std::enable_shared_from_this, + virtual RemoteStoreConfig, + virtual CommonSSHStoreConfig { using CommonSSHStoreConfig::CommonSSHStoreConfig; using RemoteStoreConfig::RemoteStoreConfig; @@ -18,44 +20,44 @@ struct SSHStoreConfig : virtual RemoteStoreConfig, virtual CommonSSHStoreConfig const Setting remoteProgram{ this, {"nix-daemon"}, "remote-program", "Path to the `nix-daemon` executable on the remote machine."}; - const std::string name() override + static const std::string name() { return "Experimental SSH Store"; } - static std::set uriSchemes() + static StringSet uriSchemes() { return {"ssh-ng"}; } - std::string doc() override; + static std::string doc(); + + ref openStore() const override; }; struct MountedSSHStoreConfig : virtual SSHStoreConfig, virtual LocalFSStoreConfig { - using LocalFSStoreConfig::LocalFSStoreConfig; - using SSHStoreConfig::SSHStoreConfig; - MountedSSHStoreConfig(StringMap params); - MountedSSHStoreConfig(std::string_view scheme, std::string_view host, StringMap params); - const std::string name() override + static const std::string name() { return "Experimental SSH Store with filesystem mounted"; } - static std::set uriSchemes() + static StringSet uriSchemes() { return {"mounted-ssh-ng"}; } - std::string doc() override; + static std::string doc(); - std::optional experimentalFeature() const override + static std::optional experimentalFeature() { return ExperimentalFeature::MountedSSHStore; } + + ref openStore() const override; }; } diff --git a/src/libstore/ssh.hh b/src/libstore/include/nix/store/ssh.hh similarity index 72% rename from src/libstore/ssh.hh rename to src/libstore/include/nix/store/ssh.hh index 85be704ec9d..be9cf0c48b6 100644 --- a/src/libstore/ssh.hh +++ b/src/libstore/include/nix/store/ssh.hh @@ -1,9 +1,9 @@ #pragma once ///@file -#include "sync.hh" -#include "processes.hh" -#include "file-system.hh" +#include "nix/util/sync.hh" +#include "nix/util/processes.hh" +#include "nix/util/file-system.hh" namespace nix { @@ -54,6 +54,18 @@ public: Pid sshPid; #endif AutoCloseFD out, in; + + /** + * Try to set the buffer size in both directions to the + * designated amount, if possible. If not possible, does + * nothing. + * + * Current implementation is to use `fcntl` with `F_SETPIPE_SZ`, + * which is Linux-only. For this implementation, `size` must + * convertible to an `int`. In other words, it must be within + * `[0, INT_MAX]`. + */ + void trySetBufferSize(size_t size); }; /** diff --git a/src/libstore/store-api.hh b/src/libstore/include/nix/store/store-api.hh similarity index 88% rename from src/libstore/store-api.hh rename to src/libstore/include/nix/store/store-api.hh index f45012061ff..e0a3e67d13b 100644 --- a/src/libstore/store-api.hh +++ b/src/libstore/include/nix/store/store-api.hh @@ -1,20 +1,20 @@ #pragma once ///@file -#include "path.hh" -#include "derived-path.hh" -#include "hash.hh" -#include "content-address.hh" -#include "serialise.hh" -#include "lru-cache.hh" -#include "sync.hh" -#include "globals.hh" -#include "config.hh" -#include "path-info.hh" -#include "repair-flag.hh" -#include "store-dir-config.hh" -#include "store-reference.hh" -#include "source-path.hh" +#include "nix/store/path.hh" +#include "nix/store/derived-path.hh" +#include "nix/util/hash.hh" +#include "nix/store/content-address.hh" +#include "nix/util/serialise.hh" +#include "nix/util/lru-cache.hh" +#include "nix/util/sync.hh" +#include "nix/store/globals.hh" +#include "nix/util/configuration.hh" +#include "nix/store/path-info.hh" +#include "nix/util/repair-flag.hh" +#include "nix/store/store-dir-config.hh" +#include "nix/store/store-reference.hh" +#include "nix/util/source-path.hh" #include #include @@ -26,32 +26,6 @@ namespace nix { -/** - * About the class hierarchy of the store types: - * - * Each store type `Foo` consists of two classes: - * - * 1. A class `FooConfig : virtual StoreConfig` that contains the configuration - * for the store - * - * It should only contain members of type `const Setting` (or subclasses - * of it) and inherit the constructors of `StoreConfig` - * (`using StoreConfig::StoreConfig`). - * - * 2. A class `Foo : virtual Store, virtual FooConfig` that contains the - * implementation of the store. - * - * This class is expected to have a constructor `Foo(const Params & params)` - * that calls `StoreConfig(params)` (otherwise you're gonna encounter an - * `assertion failure` when trying to instantiate it). - * - * You can then register the new store using: - * - * ``` - * cpp static RegisterStoreImplementation regStore; - * ``` - */ - MakeError(SubstError, Error); /** * denotes a permanent build failure @@ -97,27 +71,60 @@ struct KeyedBuildResult; typedef std::map> StorePathCAMap; -struct StoreConfig : public StoreDirConfig +/** + * Information about what paths will be built or substituted, returned + * by Store::queryMissing(). + */ +struct MissingPaths { - using Params = StoreReference::Params; + StorePathSet willBuild; + StorePathSet willSubstitute; + StorePathSet unknown; + uint64_t downloadSize{0}; + uint64_t narSize{0}; +}; +/** + * About the class hierarchy of the store types: + * + * Each store type `Foo` consists of two classes: + * + * 1. A class `FooConfig : virtual StoreConfig` that contains the configuration + * for the store + * + * It should only contain members of type `Setting` (or subclasses + * of it) and inherit the constructors of `StoreConfig` + * (`using StoreConfig::StoreConfig`). + * + * 2. A class `Foo : virtual Store` that contains the + * implementation of the store. + * + * This class is expected to have: + * + * 1. an alias `using Config = FooConfig;` + * + * 2. a constructor `Foo(ref params)`. + * + * You can then register the new store using: + * + * ``` + * cpp static RegisterStoreImplementation regStore; + * ``` + */ +struct StoreConfig : public StoreDirConfig +{ using StoreDirConfig::StoreDirConfig; StoreConfig() = delete; - static StringSet getDefaultSystemFeatures(); - virtual ~StoreConfig() { } - /** - * The name of this type of store. - */ - virtual const std::string name() = 0; + static StringSet getDefaultSystemFeatures(); /** * Documentation for this type of store. */ - virtual std::string doc() + static std::string doc() { return ""; } @@ -126,15 +133,15 @@ struct StoreConfig : public StoreDirConfig * An experimental feature this type store is gated, if it is to be * experimental. */ - virtual std::optional experimentalFeature() const + static std::optional experimentalFeature() { return std::nullopt; } - const Setting pathInfoCacheSize{this, 65536, "path-info-cache-size", + Setting pathInfoCacheSize{this, 65536, "path-info-cache-size", "Size of the in-memory store path metadata cache."}; - const Setting isTrusted{this, false, "trusted", + Setting isTrusted{this, false, "trusted", R"( Whether paths from this store can be used as substitutes even if they are not signed by a key listed in the @@ -163,10 +170,38 @@ struct StoreConfig : public StoreDirConfig {}, // Don't document the machine-specific default value false}; + + /** + * Open a store of the type corresponding to this configuration + * type. + */ + virtual ref openStore() const = 0; }; -class Store : public std::enable_shared_from_this, public virtual StoreConfig +/** + * A Store (client) + * + * This is an interface type allowing for create and read operations on + * a collection of store objects, and also building new store objects + * from `Derivation`s. See the manual for further details. + * + * "client" used is because this is just one view/actor onto an + * underlying resource, which could be an external process (daemon + * server), file system state, etc. + */ +class Store : public std::enable_shared_from_this, public MixStoreDirMethods { +public: + + using Config = StoreConfig; + + const Config & config; + + /** + * @note Avoid churn, since we used to inherit from `Config`. + */ + operator const Config &() const { return config; } + protected: struct PathInfoCacheValue { @@ -205,7 +240,7 @@ protected: std::shared_ptr diskCache; - Store(const Params & params); + Store(const Store::Config & config); public: /** @@ -359,7 +394,7 @@ public: /** * Query the mapping outputName => outputPath for the given - * derivation. All outputs are mentioned so ones mising the mapping + * derivation. All outputs are mentioned so ones missing the mapping * are mapped to `std::nullopt`. */ virtual std::map> queryPartialDerivationOutputMap( @@ -425,7 +460,7 @@ public: CheckSigsFlag checkSigs = CheckSigs); virtual void addMultipleToStore( - PathsSource & pathsToCopy, + PathsSource && pathsToCopy, Activity & act, RepairFlag repair = NoRepair, CheckSigsFlag checkSigs = CheckSigs); @@ -622,6 +657,14 @@ public: virtual void addSignatures(const StorePath & storePath, const StringSet & sigs) { unsupported("addSignatures"); } + /** + * Add signatures to a ValidPathInfo or Realisation using the secret keys + * specified by the ‘secret-key-files’ option. + */ + void signPathInfo(ValidPathInfo & info); + + void signRealisation(Realisation &); + /* Utility functions. */ /** @@ -663,9 +706,7 @@ public: * derivations that will be built, and the set of output paths that * will be substituted. */ - virtual void queryMissing(const std::vector & targets, - StorePathSet & willBuild, StorePathSet & willSubstitute, StorePathSet & unknown, - uint64_t & downloadSize, uint64_t & narSize); + virtual MissingPaths queryMissing(const std::vector & targets); /** * Sort a set of paths topologically under the references @@ -715,7 +756,7 @@ public: /** * Given a store path, return the realisation actually used in the realisation of this path: - * - If the path is a content-addressed derivation, try to resolve it + * - If the path is a content-addressing derivation, try to resolve it * - Otherwise, find one of its derivers */ std::optional getBuildDerivationPath(const StorePath &); @@ -778,7 +819,7 @@ protected: /** * Helper for methods that are not unsupported: this is used for - * default definitions for virtual methods that are meant to be overriden. + * default definitions for virtual methods that are meant to be overridden. * * @todo Using this should be a last resort. It is better to make * the method "virtual pure" and/or move it to a subclass. @@ -857,74 +898,6 @@ StorePath resolveDerivedPath(Store &, const SingleDerivedPath &, Store * evalSto OutputPathMap resolveDerivedPath(Store &, const DerivedPath::Built &, Store * evalStore = nullptr); -/** - * @return a Store object to access the Nix store denoted by - * ‘uri’ (slight misnomer...). - */ -ref openStore(StoreReference && storeURI); - - -/** - * Opens the store at `uri`, where `uri` is in the format expected by `StoreReference::parse` - - */ -ref openStore(const std::string & uri = settings.storeUri.get(), - const Store::Params & extraParams = Store::Params()); - - -/** - * @return the default substituter stores, defined by the - * ‘substituters’ option and various legacy options. - */ -std::list> getDefaultSubstituters(); - -struct StoreFactory -{ - std::set uriSchemes; - /** - * The `authorityPath` parameter is `/`, or really - * whatever comes after `://` and before `?`. - */ - std::function ( - std::string_view scheme, - std::string_view authorityPath, - const Store::Params & params)> create; - std::function ()> getConfig; -}; - -struct Implementations -{ - static std::vector * registered; - - template - static void add() - { - if (!registered) registered = new std::vector(); - StoreFactory factory{ - .uriSchemes = TConfig::uriSchemes(), - .create = - ([](auto scheme, auto uri, auto & params) - -> std::shared_ptr - { return std::make_shared(scheme, uri, params); }), - .getConfig = - ([]() - -> std::shared_ptr - { return std::make_shared(StringMap({})); }) - }; - registered->push_back(factory); - } -}; - -template -struct RegisterStoreImplementation -{ - RegisterStoreImplementation() - { - Implementations::add(); - } -}; - - /** * Display a set of paths in human-readable form (i.e., between quotes * and separated by commas). diff --git a/src/libstore/store-cast.hh b/src/libstore/include/nix/store/store-cast.hh similarity index 93% rename from src/libstore/store-cast.hh rename to src/libstore/include/nix/store/store-cast.hh index 2473e72c5c4..0bf61bb7733 100644 --- a/src/libstore/store-cast.hh +++ b/src/libstore/include/nix/store/store-cast.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "store-api.hh" +#include "nix/store/store-api.hh" namespace nix { diff --git a/src/libstore/store-dir-config.hh b/src/libstore/include/nix/store/store-dir-config.hh similarity index 67% rename from src/libstore/store-dir-config.hh rename to src/libstore/include/nix/store/store-dir-config.hh index fd4332b918f..14e3e7db84e 100644 --- a/src/libstore/store-dir-config.hh +++ b/src/libstore/include/nix/store/store-dir-config.hh @@ -1,10 +1,10 @@ #pragma once -#include "path.hh" -#include "hash.hh" -#include "content-address.hh" -#include "globals.hh" -#include "config.hh" +#include "nix/store/path.hh" +#include "nix/util/hash.hh" +#include "nix/store/content-address.hh" +#include "nix/store/globals.hh" +#include "nix/util/configuration.hh" #include #include @@ -18,22 +18,18 @@ struct SourcePath; MakeError(BadStorePath, Error); MakeError(BadStorePathName, BadStorePath); -struct StoreDirConfig : public Config +/** + * @todo This should just be part of `StoreDirConfig`. However, it would + * be a huge amount of churn if `Store` didn't have these methods + * anymore, forcing a bunch of code to go from `store.method(...)` to + * `store.config.method(...)`. + * + * So we instead pull out the methods into their own mix-in, so can put + * them directly on the Store too. + */ +struct MixStoreDirMethods { - using Config::Config; - - StoreDirConfig() = delete; - - virtual ~StoreDirConfig() = default; - - const PathSetting storeDir_{this, settings.nixStore, - "store", - R"( - Logical location of the Nix store, usually - `/nix/store`. Note that you can only copy store paths - between stores if they have the same `store` setting. - )"}; - const Path storeDir = storeDir_; + const Path & storeDir; // pure methods @@ -56,7 +52,7 @@ struct StoreDirConfig : public Config * Display a set of paths in human-readable form (i.e., between quotes * and separated by commas). */ - std::string showPaths(const StorePathSet & paths); + std::string showPaths(const StorePathSet & paths) const; /** * @return true if *path* is in the Nix store (but not the Nix @@ -93,7 +89,7 @@ struct StoreDirConfig : public Config /** * Read-only variant of addToStore(). It returns the store - * path for the given file sytem object. + * path for the given file system object. */ std::pair computeStorePath( std::string_view name, @@ -104,4 +100,38 @@ struct StoreDirConfig : public Config PathFilter & filter = defaultPathFilter) const; }; +/** + * Need to make this a separate class so I can get the right + * initialization order in the constructor for `StoreDirConfig`. + */ +struct StoreDirConfigBase : Config +{ + using Config::Config; + + const PathSetting storeDir_{this, settings.nixStore, + "store", + R"( + Logical location of the Nix store, usually + `/nix/store`. Note that you can only copy store paths + between stores if they have the same `store` setting. + )"}; +}; + +/** + * The order of `StoreDirConfigBase` and then `MixStoreDirMethods` is + * very important. This ensures that `StoreDirConfigBase::storeDir_` + * is initialized before we have our one chance (because references are + * immutable) to initialize `MixStoreDirMethods::storeDir`. + */ +struct StoreDirConfig : StoreDirConfigBase, MixStoreDirMethods +{ + using Params = StringMap; + + StoreDirConfig(const Params & params); + + StoreDirConfig() = delete; + + virtual ~StoreDirConfig() = default; +}; + } diff --git a/src/libstore/include/nix/store/store-open.hh b/src/libstore/include/nix/store/store-open.hh new file mode 100644 index 00000000000..7c1cda5bebf --- /dev/null +++ b/src/libstore/include/nix/store/store-open.hh @@ -0,0 +1,43 @@ +#pragma once +/** + * @file + * + * For opening a store described by an `StoreReference`, which is an "untyped" + * notion which needs to be decoded against a collection of specific + * implementations. + * + * For consumers of the store registration machinery defined in + * `store-registration.hh`. Not needed by store implementation definitions, or + * usages of a given `Store` which will be passed in. + */ + +#include "nix/store/store-api.hh" + +namespace nix { + +/** + * @return The store config denoted by `storeURI` (slight misnomer...). + */ +ref resolveStoreConfig(StoreReference && storeURI); + +/** + * @return a Store object to access the Nix store denoted by + * ‘uri’ (slight misnomer...). + */ +ref openStore(StoreReference && storeURI); + +/** + * Opens the store at `uri`, where `uri` is in the format expected by + * `StoreReference::parse` + */ +ref openStore( + const std::string & uri = settings.storeUri.get(), + const StoreReference::Params & extraParams = StoreReference::Params()); + +/** + * @return the default substituter stores, defined by the + * ‘substituters’ option and various legacy options. + */ +std::list> getDefaultSubstituters(); + +} diff --git a/src/libstore/store-reference.hh b/src/libstore/include/nix/store/store-reference.hh similarity index 94% rename from src/libstore/store-reference.hh rename to src/libstore/include/nix/store/store-reference.hh index 7100a1db095..c1b681ba16d 100644 --- a/src/libstore/store-reference.hh +++ b/src/libstore/include/nix/store/store-reference.hh @@ -3,13 +3,13 @@ #include -#include "types.hh" +#include "nix/util/types.hh" namespace nix { /** * A parsed Store URI (URI is a slight misnomer...), parsed but not yet - * resolved to a specific instance and query parms validated. + * resolved to a specific instance and query params validated. * * Supported values are: * @@ -41,7 +41,7 @@ namespace nix { */ struct StoreReference { - using Params = std::map; + using Params = StringMap; /** * Special store reference `""` or `"auto"` diff --git a/src/libstore/include/nix/store/store-registration.hh b/src/libstore/include/nix/store/store-registration.hh new file mode 100644 index 00000000000..17298118e5a --- /dev/null +++ b/src/libstore/include/nix/store/store-registration.hh @@ -0,0 +1,88 @@ +#pragma once +/** + * @file + * + * Infrastructure for "registering" store implementations. Used by the + * store implementation definitions themselves but not by consumers of + * those implementations. + * + * Consumers of an arbitrary store from a URL/JSON configuration instead + * just need the definitions `nix/store/store-open.hh`; those do use this + * but only as an implementation. Consumers of a specific extra type of + * store can skip both these, and just use the definition of the store + * in question directly. + */ + +#include "nix/store/store-api.hh" + +namespace nix { + +struct StoreFactory +{ + /** + * Documentation for this type of store. + */ + std::string doc; + + /** + * URIs with these schemes should be handled by this factory + */ + StringSet uriSchemes; + + /** + * An experimental feature this type store is gated, if it is to be + * experimental. + */ + std::optional experimentalFeature; + + /** + * The `authorityPath` parameter is `/`, or really + * whatever comes after `://` and before `?`. + */ + std::function( + std::string_view scheme, std::string_view authorityPath, const Store::Config::Params & params)> + parseConfig; + + /** + * Just for dumping the defaults. Kind of awkward this exists, + * because it means we cannot require fields to be manually + * specified so easily. + */ + std::function()> getConfig; +}; + +struct Implementations +{ + using Map = std::map; + + static Map & registered(); + + template + static void add() + { + StoreFactory factory{ + .doc = TConfig::doc(), + .uriSchemes = TConfig::uriSchemes(), + .experimentalFeature = TConfig::experimentalFeature(), + .parseConfig = ([](auto scheme, auto uri, auto & params) -> ref { + return make_ref(scheme, uri, params); + }), + .getConfig = ([]() -> ref { return make_ref(Store::Config::Params{}); }), + }; + auto [it, didInsert] = registered().insert({TConfig::name(), std::move(factory)}); + if (!didInsert) { + throw Error("Already registered store with name '%s'", it->first); + } + } +}; + +template +struct RegisterStoreImplementation +{ + RegisterStoreImplementation() + { + Implementations::add(); + } +}; + +} diff --git a/src/libstore/uds-remote-store.hh b/src/libstore/include/nix/store/uds-remote-store.hh similarity index 64% rename from src/libstore/uds-remote-store.hh rename to src/libstore/include/nix/store/uds-remote-store.hh index a8e57166416..e9793a9ee55 100644 --- a/src/libstore/uds-remote-store.hh +++ b/src/libstore/include/nix/store/uds-remote-store.hh @@ -1,13 +1,16 @@ #pragma once ///@file -#include "remote-store.hh" -#include "remote-store-connection.hh" -#include "indirect-root-store.hh" +#include "nix/store/remote-store.hh" +#include "nix/store/remote-store-connection.hh" +#include "nix/store/indirect-root-store.hh" namespace nix { -struct UDSRemoteStoreConfig : virtual LocalFSStoreConfig, virtual RemoteStoreConfig +struct UDSRemoteStoreConfig : + std::enable_shared_from_this, + virtual LocalFSStoreConfig, + virtual RemoteStoreConfig { // TODO(fzakaria): Delete this constructor once moved over to the factory pattern // outlined in https://github.com/NixOS/nix/issues/10766 @@ -22,9 +25,11 @@ struct UDSRemoteStoreConfig : virtual LocalFSStoreConfig, virtual RemoteStoreCon std::string_view authority, const Params & params); - const std::string name() override { return "Local Daemon Store"; } + UDSRemoteStoreConfig(const Params & params); - std::string doc() override; + static const std::string name() { return "Local Daemon Store"; } + + static std::string doc(); /** * The path to the unix domain socket. @@ -34,32 +39,21 @@ struct UDSRemoteStoreConfig : virtual LocalFSStoreConfig, virtual RemoteStoreCon */ Path path; -protected: - static constexpr char const * scheme = "unix"; + static StringSet uriSchemes() + { return {"unix"}; } -public: - static std::set uriSchemes() - { return {scheme}; } + ref openStore() const override; }; -class UDSRemoteStore : public virtual UDSRemoteStoreConfig - , public virtual IndirectRootStore - , public virtual RemoteStore +struct UDSRemoteStore : + virtual IndirectRootStore, + virtual RemoteStore { -public: + using Config = UDSRemoteStoreConfig; - /** - * @deprecated This is the old API to construct the store. - */ - UDSRemoteStore(const Params & params); + ref config; - /** - * @param authority is the socket path. - */ - UDSRemoteStore( - std::string_view scheme, - std::string_view authority, - const Params & params); + UDSRemoteStore(ref); std::string getUri() override; diff --git a/src/libstore/worker-protocol-connection.hh b/src/libstore/include/nix/store/worker-protocol-connection.hh similarity index 88% rename from src/libstore/worker-protocol-connection.hh rename to src/libstore/include/nix/store/worker-protocol-connection.hh index c2f446db1d9..ce7e9aef47c 100644 --- a/src/libstore/worker-protocol-connection.hh +++ b/src/libstore/include/nix/store/worker-protocol-connection.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "worker-protocol.hh" -#include "store-api.hh" +#include "nix/store/worker-protocol.hh" +#include "nix/store/store-api.hh" namespace nix { @@ -26,7 +26,7 @@ struct WorkerProto::BasicConnection /** * The set of features that both sides support. */ - std::set features; + FeatureSet features; /** * Coercion to `WorkerProto::ReadConn`. This makes it easy to use the @@ -92,15 +92,12 @@ struct WorkerProto::BasicClientConnection : WorkerProto::BasicConnection * @param supportedFeatures The protocol features that we support. */ // FIXME: this should probably be a constructor. - static std::tuple> handshake( - BufferedSink & to, - Source & from, - WorkerProto::Version localVersion, - const std::set & supportedFeatures); + static std::tuple handshake( + BufferedSink & to, Source & from, WorkerProto::Version localVersion, const FeatureSet & supportedFeatures); /** * After calling handshake, must call this to exchange some basic - * information abou the connection. + * information about the connection. */ ClientHandshakeInfo postHandshake(const StoreDirConfig & store); @@ -155,15 +152,12 @@ struct WorkerProto::BasicServerConnection : WorkerProto::BasicConnection * @param supportedFeatures The protocol features that we support. */ // FIXME: this should probably be a constructor. - static std::tuple> handshake( - BufferedSink & to, - Source & from, - WorkerProto::Version localVersion, - const std::set & supportedFeatures); + static std::tuple handshake( + BufferedSink & to, Source & from, WorkerProto::Version localVersion, const FeatureSet & supportedFeatures); /** * After calling handshake, must call this to exchange some basic - * information abou the connection. + * information about the connection. */ void postHandshake(const StoreDirConfig & store, const ClientHandshakeInfo & info); }; diff --git a/src/libstore/worker-protocol-impl.hh b/src/libstore/include/nix/store/worker-protocol-impl.hh similarity index 84% rename from src/libstore/worker-protocol-impl.hh rename to src/libstore/include/nix/store/worker-protocol-impl.hh index 87398df90c9..23e6068e9bb 100644 --- a/src/libstore/worker-protocol-impl.hh +++ b/src/libstore/include/nix/store/worker-protocol-impl.hh @@ -4,12 +4,12 @@ * * Template implementations (as opposed to mere declarations). * - * This file is an exmample of the "impl.hh" pattern. See the + * This file is an example of the "impl.hh" pattern. See the * contributing guide. */ -#include "worker-protocol.hh" -#include "length-prefixed-protocol-helper.hh" +#include "nix/store/worker-protocol.hh" +#include "nix/store/length-prefixed-protocol-helper.hh" namespace nix { @@ -26,7 +26,9 @@ namespace nix { } WORKER_USE_LENGTH_PREFIX_SERIALISER(template, std::vector) -WORKER_USE_LENGTH_PREFIX_SERIALISER(template, std::set) +#define COMMA_ , +WORKER_USE_LENGTH_PREFIX_SERIALISER(template, std::set) +#undef COMMA_ WORKER_USE_LENGTH_PREFIX_SERIALISER(template, std::tuple) #define WORKER_USE_LENGTH_PREFIX_SERIALISER_COMMA , diff --git a/src/libstore/worker-protocol.hh b/src/libstore/include/nix/store/worker-protocol.hh similarity index 96% rename from src/libstore/worker-protocol.hh rename to src/libstore/include/nix/store/worker-protocol.hh index c356fa1bf37..9630a88c063 100644 --- a/src/libstore/worker-protocol.hh +++ b/src/libstore/include/nix/store/worker-protocol.hh @@ -3,7 +3,7 @@ #include -#include "common-protocol.hh" +#include "nix/store/common-protocol.hh" namespace nix { @@ -89,7 +89,7 @@ struct WorkerProto struct BasicServerConnection; /** - * Extra information provided as part of protocol negotation. + * Extra information provided as part of protocol negotiation. */ struct ClientHandshakeInfo; @@ -135,8 +135,9 @@ struct WorkerProto } using Feature = std::string; + using FeatureSet = std::set>; - static const std::set allFeatures; + static const FeatureSet allFeatures; }; enum struct WorkerProto::Op : uint64_t @@ -272,12 +273,12 @@ DECLARE_WORKER_SERIALISER(WorkerProto::ClientHandshakeInfo); template DECLARE_WORKER_SERIALISER(std::vector); -template -DECLARE_WORKER_SERIALISER(std::set); +#define COMMA_ , +template +DECLARE_WORKER_SERIALISER(std::set); template DECLARE_WORKER_SERIALISER(std::tuple); -#define COMMA_ , template DECLARE_WORKER_SERIALISER(std::map); #undef COMMA_ diff --git a/src/libstore/indirect-root-store.cc b/src/libstore/indirect-root-store.cc index 844d0d6edad..e23c01e5de5 100644 --- a/src/libstore/indirect-root-store.cc +++ b/src/libstore/indirect-root-store.cc @@ -1,4 +1,4 @@ -#include "indirect-root-store.hh" +#include "nix/store/indirect-root-store.hh" namespace nix { diff --git a/src/libstore/keys.cc b/src/libstore/keys.cc index 668725fc7e8..9abea952043 100644 --- a/src/libstore/keys.cc +++ b/src/libstore/keys.cc @@ -1,6 +1,6 @@ -#include "file-system.hh" -#include "globals.hh" -#include "keys.hh" +#include "nix/util/file-system.hh" +#include "nix/store/globals.hh" +#include "nix/store/keys.hh" namespace nix { diff --git a/src/libstore/legacy-ssh-store.cc b/src/libstore/legacy-ssh-store.cc index eac360a4f7a..9ec9e6eec19 100644 --- a/src/libstore/legacy-ssh-store.cc +++ b/src/libstore/legacy-ssh-store.cc @@ -1,17 +1,18 @@ -#include "legacy-ssh-store.hh" -#include "common-ssh-store-config.hh" -#include "archive.hh" -#include "pool.hh" -#include "remote-store.hh" -#include "serve-protocol.hh" -#include "serve-protocol-connection.hh" -#include "serve-protocol-impl.hh" -#include "build-result.hh" -#include "store-api.hh" -#include "path-with-outputs.hh" -#include "ssh.hh" -#include "derivations.hh" -#include "callback.hh" +#include "nix/store/legacy-ssh-store.hh" +#include "nix/store/common-ssh-store-config.hh" +#include "nix/util/archive.hh" +#include "nix/util/pool.hh" +#include "nix/store/remote-store.hh" +#include "nix/store/serve-protocol.hh" +#include "nix/store/serve-protocol-connection.hh" +#include "nix/store/serve-protocol-impl.hh" +#include "nix/store/build-result.hh" +#include "nix/store/store-api.hh" +#include "nix/store/path-with-outputs.hh" +#include "nix/store/ssh.hh" +#include "nix/store/derivations.hh" +#include "nix/util/callback.hh" +#include "nix/store/store-registration.hh" namespace nix { @@ -38,23 +39,19 @@ struct LegacySSHStore::Connection : public ServeProto::BasicClientConnection bool good = true; }; -LegacySSHStore::LegacySSHStore( - std::string_view scheme, - std::string_view host, - const Params & params) - : StoreConfig(params) - , CommonSSHStoreConfig(scheme, host, params) - , LegacySSHStoreConfig(scheme, host, params) - , Store(params) + +LegacySSHStore::LegacySSHStore(ref config) + : Store{*config} + , config{config} , connections(make_ref>( - std::max(1, (int) maxConnections), + std::max(1, (int) config->maxConnections), [this]() { return openConnection(); }, [](const ref & r) { return r->good; } )) - , master(createSSHMaster( + , master(config->createSSHMaster( // Use SSH master only if using more than 1 connection. connections->capacity() > 1, - logFD)) + config->logFD)) { } @@ -62,14 +59,17 @@ LegacySSHStore::LegacySSHStore( ref LegacySSHStore::openConnection() { auto conn = make_ref(); - Strings command = remoteProgram.get(); + Strings command = config->remoteProgram.get(); command.push_back("--serve"); command.push_back("--write"); - if (remoteStore.get() != "") { + if (config->remoteStore.get() != "") { command.push_back("--store"); - command.push_back(remoteStore.get()); + command.push_back(config->remoteStore.get()); + } + conn->sshConn = master.startCommand(std::move(command), std::list{config->extraSshArgs}); + if (config->connPipeSize) { + conn->sshConn->trySetBufferSize(*config->connPipeSize); } - conn->sshConn = master.startCommand(std::move(command)); conn->to = FdSink(conn->sshConn->in.get()); conn->from = FdSource(conn->sshConn->out.get()); @@ -77,7 +77,7 @@ ref LegacySSHStore::openConnection() TeeSource tee(conn->from, saved); try { conn->remoteVersion = ServeProto::BasicClientConnection::handshake( - conn->to, tee, SERVE_PROTOCOL_VERSION, host); + conn->to, tee, SERVE_PROTOCOL_VERSION, config->host); } catch (SerialisationError & e) { // in.close(): Don't let the remote block on us not writing. conn->sshConn->in.close(); @@ -86,9 +86,9 @@ ref LegacySSHStore::openConnection() tee.drainInto(nullSink); } throw Error("'nix-store --serve' protocol mismatch from '%s', got '%s'", - host, chomp(saved.s)); + config->host, chomp(saved.s)); } catch (EndOfFile & e) { - throw Error("cannot connect to '%1%'", host); + throw Error("cannot connect to '%1%'", config->host); } return conn; @@ -97,22 +97,34 @@ ref LegacySSHStore::openConnection() std::string LegacySSHStore::getUri() { - return *uriSchemes().begin() + "://" + host; + return *Config::uriSchemes().begin() + "://" + config->host; } +std::map LegacySSHStore::queryPathInfosUncached( + const StorePathSet & paths) +{ + auto conn(connections->get()); + + /* No longer support missing NAR hash */ + assert(GET_PROTOCOL_MINOR(conn->remoteVersion) >= 4); + + debug("querying remote host '%s' for info on '%s'", config->host, concatStringsSep(", ", printStorePathSet(paths))); + + auto infos = conn->queryPathInfos(*this, paths); + + for (const auto & [_, info] : infos) { + if (info.narHash == Hash::dummy) + throw Error("NAR hash is now mandatory"); + } + + return infos; +} void LegacySSHStore::queryPathInfoUncached(const StorePath & path, Callback> callback) noexcept { try { - auto conn(connections->get()); - - /* No longer support missing NAR hash */ - assert(GET_PROTOCOL_MINOR(conn->remoteVersion) >= 4); - - debug("querying remote host '%s' for info on '%s'", host, printStorePath(path)); - - auto infos = conn->queryPathInfos(*this, {path}); + auto infos = queryPathInfosUncached({path}); switch (infos.size()) { case 0: @@ -120,9 +132,6 @@ void LegacySSHStore::queryPathInfoUncached(const StorePath & path, case 1: { auto & [path2, info] = *infos.begin(); - if (info.narHash == Hash::dummy) - throw Error("NAR hash is now mandatory"); - assert(path == path2); return callback(std::make_shared( std::move(path), @@ -139,7 +148,7 @@ void LegacySSHStore::queryPathInfoUncached(const StorePath & path, void LegacySSHStore::addToStore(const ValidPathInfo & info, Source & source, RepairFlag repair, CheckSigsFlag checkSigs) { - debug("adding path '%s' to remote host '%s'", printStorePath(info.path), host); + debug("adding path '%s' to remote host '%s'", printStorePath(info.path), config->host); auto conn(connections->get()); @@ -166,7 +175,7 @@ void LegacySSHStore::addToStore(const ValidPathInfo & info, Source & source, conn->to.flush(); if (readInt(conn->from) != 1) - throw Error("failed to add path '%s' to remote host '%s'", printStorePath(info.path), host); + throw Error("failed to add path '%s' to remote host '%s'", printStorePath(info.path), config->host); } else { @@ -193,13 +202,19 @@ void LegacySSHStore::addToStore(const ValidPathInfo & info, Source & source, void LegacySSHStore::narFromPath(const StorePath & path, Sink & sink) { - auto conn(connections->get()); - conn->narFromPath(*this, path, [&](auto & source) { + narFromPath(path, [&](auto & source) { copyNAR(source, sink); }); } +void LegacySSHStore::narFromPath(const StorePath & path, std::function fun) +{ + auto conn(connections->get()); + conn->narFromPath(*this, path, fun); +} + + static ServeProto::BuildOptions buildSettings() { return { @@ -223,6 +238,19 @@ BuildResult LegacySSHStore::buildDerivation(const StorePath & drvPath, const Bas return conn->getBuildDerivationResponse(*this); } +std::function LegacySSHStore::buildDerivationAsync( + const StorePath & drvPath, const BasicDerivation & drv, + const ServeProto::BuildOptions & options) +{ + // Until we have C++23 std::move_only_function + auto conn = std::make_shared::Handle>(connections->get()); + (*conn)->putBuildDerivationRequest(*this, drvPath, drv, options); + + return [this,conn]() -> BuildResult { + return (*conn)->getBuildDerivationResponse(*this); + }; +} + void LegacySSHStore::buildPaths(const std::vector & drvPaths, BuildMode buildMode, std::shared_ptr evalStore) { @@ -294,6 +322,32 @@ StorePathSet LegacySSHStore::queryValidPaths(const StorePathSet & paths, } +StorePathSet LegacySSHStore::queryValidPaths(const StorePathSet & paths, + bool lock, SubstituteFlag maybeSubstitute) +{ + auto conn(connections->get()); + return conn->queryValidPaths(*this, + lock, paths, maybeSubstitute); +} + + +void LegacySSHStore::addMultipleToStoreLegacy(Store & srcStore, const StorePathSet & paths) +{ + auto conn(connections->get()); + conn->to << ServeProto::Command::ImportPaths; + try { + srcStore.exportPaths(paths, conn->to); + } catch (...) { + conn->good = false; + throw; + } + conn->to.flush(); + + if (readInt(conn->from) != 1) + throw Error("remote machine failed to import closure"); +} + + void LegacySSHStore::connect() { auto conn(connections->get()); @@ -307,16 +361,43 @@ unsigned int LegacySSHStore::getProtocol() } +pid_t LegacySSHStore::getConnectionPid() +{ + auto conn(connections->get()); +#ifndef _WIN32 + return conn->sshConn->sshPid; +#else + // TODO: Implement + return 0; +#endif +} + + +LegacySSHStore::ConnectionStats LegacySSHStore::getConnectionStats() +{ + auto conn(connections->get()); + return { + .bytesReceived = conn->from.read, + .bytesSent = conn->to.written, + }; +} + + /** * The legacy ssh protocol doesn't support checking for trusted-user. * Try using ssh-ng:// instead if you want to know. */ -std::optional isTrustedClient() +std::optional LegacySSHStore::isTrustedClient() { return std::nullopt; } -static RegisterStoreImplementation regLegacySSHStore; +ref LegacySSHStore::Config::openStore() const { + return make_ref(ref{shared_from_this()}); +} + + +static RegisterStoreImplementation regLegacySSHStore; } diff --git a/src/libstore/linux/fchmodat2-compat.hh b/src/libstore/linux/fchmodat2-compat.hh index fd03b9ed5aa..907695c315b 100644 --- a/src/libstore/linux/fchmodat2-compat.hh +++ b/src/libstore/linux/fchmodat2-compat.hh @@ -1,3 +1,6 @@ +#include "store-config-private.hh" +#include + /* * Determine the syscall number for `fchmodat2`. * diff --git a/src/libstore/linux/include/nix/store/meson.build b/src/libstore/linux/include/nix/store/meson.build new file mode 100644 index 00000000000..c8e6a826863 --- /dev/null +++ b/src/libstore/linux/include/nix/store/meson.build @@ -0,0 +1,6 @@ +include_dirs += include_directories('../..') + +headers += files( + 'personality.hh', + # hack for trailing newline +) diff --git a/src/libstore/linux/personality.hh b/src/libstore/linux/include/nix/store/personality.hh similarity index 100% rename from src/libstore/linux/personality.hh rename to src/libstore/linux/include/nix/store/personality.hh diff --git a/src/libstore/linux/meson.build b/src/libstore/linux/meson.build index 0c494b5d62e..5771cead501 100644 --- a/src/libstore/linux/meson.build +++ b/src/libstore/linux/meson.build @@ -1,10 +1,6 @@ sources += files( 'personality.cc', + # hack for trailing newline ) -include_dirs += include_directories('.') - -headers += files( - 'fchmodat2-compat.hh', - 'personality.hh', -) +subdir('include/nix/store') diff --git a/src/libstore/linux/personality.cc b/src/libstore/linux/personality.cc index 255d174a6cc..e87006d86f1 100644 --- a/src/libstore/linux/personality.cc +++ b/src/libstore/linux/personality.cc @@ -1,5 +1,5 @@ -#include "personality.hh" -#include "globals.hh" +#include "nix/store/personality.hh" +#include "nix/store/globals.hh" #include #include @@ -15,7 +15,7 @@ void setPersonality(std::string_view system) struct utsname utsbuf; uname(&utsbuf); if ((system == "i686-linux" - && (std::string_view(SYSTEM) == "x86_64-linux" + && (std::string_view(NIX_LOCAL_SYSTEM) == "x86_64-linux" || (!strcmp(utsbuf.sysname, "Linux") && !strcmp(utsbuf.machine, "x86_64")))) || system == "armv7l-linux" || system == "armv6l-linux" diff --git a/src/libstore/local-binary-cache-store.cc b/src/libstore/local-binary-cache-store.cc index dcc6affe4a1..2f23135fae7 100644 --- a/src/libstore/local-binary-cache-store.cc +++ b/src/libstore/local-binary-cache-store.cc @@ -1,7 +1,8 @@ -#include "local-binary-cache-store.hh" -#include "globals.hh" -#include "nar-info-disk-cache.hh" -#include "signals.hh" +#include "nix/store/local-binary-cache-store.hh" +#include "nix/store/globals.hh" +#include "nix/store/nar-info-disk-cache.hh" +#include "nix/util/signals.hh" +#include "nix/store/store-registration.hh" #include @@ -10,9 +11,9 @@ namespace nix { LocalBinaryCacheStoreConfig::LocalBinaryCacheStoreConfig( std::string_view scheme, PathView binaryCacheDir, - const Params & params) - : StoreConfig(params) - , BinaryCacheStoreConfig(params) + const StoreReference::Params & params) + : Store::Config{params} + , BinaryCacheStoreConfig{params} , binaryCacheDir(binaryCacheDir) { } @@ -26,29 +27,26 @@ std::string LocalBinaryCacheStoreConfig::doc() } -struct LocalBinaryCacheStore : virtual LocalBinaryCacheStoreConfig, virtual BinaryCacheStore +struct LocalBinaryCacheStore : + virtual BinaryCacheStore { - /** - * @param binaryCacheDir `file://` is a short-hand for `file:///` - * for now. - */ - LocalBinaryCacheStore( - std::string_view scheme, - PathView binaryCacheDir, - const Params & params) - : StoreConfig(params) - , BinaryCacheStoreConfig(params) - , LocalBinaryCacheStoreConfig(scheme, binaryCacheDir, params) - , Store(params) - , BinaryCacheStore(params) + using Config = LocalBinaryCacheStoreConfig; + + ref config; + + LocalBinaryCacheStore(ref config) + : Store{*config} + , BinaryCacheStore{*config} + , config{config} { + init(); } void init() override; std::string getUri() override { - return "file://" + binaryCacheDir; + return "file://" + config->binaryCacheDir; } protected: @@ -59,7 +57,7 @@ struct LocalBinaryCacheStore : virtual LocalBinaryCacheStoreConfig, virtual Bina std::shared_ptr> istream, const std::string & mimeType) override { - auto path2 = binaryCacheDir + "/" + path; + auto path2 = config->binaryCacheDir + "/" + path; static std::atomic counter{0}; Path tmp = fmt("%s.tmp.%d.%d", path2, getpid(), ++counter); AutoDelete del(tmp, false); @@ -72,7 +70,7 @@ struct LocalBinaryCacheStore : virtual LocalBinaryCacheStoreConfig, virtual Bina void getFile(const std::string & path, Sink & sink) override { try { - readFile(binaryCacheDir + "/" + path, sink); + readFile(config->binaryCacheDir + "/" + path, sink); } catch (SysError & e) { if (e.errNo == ENOENT) throw NoSuchBinaryCacheFile("file '%s' does not exist in binary cache", path); @@ -84,7 +82,7 @@ struct LocalBinaryCacheStore : virtual LocalBinaryCacheStoreConfig, virtual Bina { StorePathSet paths; - for (auto & entry : std::filesystem::directory_iterator{binaryCacheDir}) { + for (auto & entry : DirectoryIterator{config->binaryCacheDir}) { checkInterrupt(); auto name = entry.path().filename().string(); if (name.size() != 40 || @@ -106,20 +104,20 @@ struct LocalBinaryCacheStore : virtual LocalBinaryCacheStoreConfig, virtual Bina void LocalBinaryCacheStore::init() { - createDirs(binaryCacheDir + "/nar"); - createDirs(binaryCacheDir + "/" + realisationsPrefix); - if (writeDebugInfo) - createDirs(binaryCacheDir + "/debuginfo"); - createDirs(binaryCacheDir + "/log"); + createDirs(config->binaryCacheDir + "/nar"); + createDirs(config->binaryCacheDir + "/" + realisationsPrefix); + if (config->writeDebugInfo) + createDirs(config->binaryCacheDir + "/debuginfo"); + createDirs(config->binaryCacheDir + "/log"); BinaryCacheStore::init(); } bool LocalBinaryCacheStore::fileExists(const std::string & path) { - return pathExists(binaryCacheDir + "/" + path); + return pathExists(config->binaryCacheDir + "/" + path); } -std::set LocalBinaryCacheStoreConfig::uriSchemes() +StringSet LocalBinaryCacheStoreConfig::uriSchemes() { if (getEnv("_NIX_FORCE_HTTP") == "1") return {}; @@ -127,6 +125,13 @@ std::set LocalBinaryCacheStoreConfig::uriSchemes() return {"file"}; } -static RegisterStoreImplementation regLocalBinaryCacheStore; +ref LocalBinaryCacheStoreConfig::openStore() const { + return make_ref(ref{ + // FIXME we shouldn't actually need a mutable config + std::const_pointer_cast(shared_from_this()) + }); +} + +static RegisterStoreImplementation regLocalBinaryCacheStore; } diff --git a/src/libstore/local-binary-cache-store.hh b/src/libstore/local-binary-cache-store.hh deleted file mode 100644 index 997e8ecbb51..00000000000 --- a/src/libstore/local-binary-cache-store.hh +++ /dev/null @@ -1,23 +0,0 @@ -#include "binary-cache-store.hh" - -namespace nix { - -struct LocalBinaryCacheStoreConfig : virtual BinaryCacheStoreConfig -{ - using BinaryCacheStoreConfig::BinaryCacheStoreConfig; - - LocalBinaryCacheStoreConfig(std::string_view scheme, PathView binaryCacheDir, const Params & params); - - Path binaryCacheDir; - - const std::string name() override - { - return "Local Binary Cache Store"; - } - - static std::set uriSchemes(); - - std::string doc() override; -}; - -} diff --git a/src/libstore/local-fs-store.cc b/src/libstore/local-fs-store.cc index 5449b20eb3b..add3b04d237 100644 --- a/src/libstore/local-fs-store.cc +++ b/src/libstore/local-fs-store.cc @@ -1,10 +1,10 @@ -#include "archive.hh" -#include "posix-source-accessor.hh" -#include "store-api.hh" -#include "local-fs-store.hh" -#include "globals.hh" -#include "compression.hh" -#include "derivations.hh" +#include "nix/util/archive.hh" +#include "nix/util/posix-source-accessor.hh" +#include "nix/store/store-api.hh" +#include "nix/store/local-fs-store.hh" +#include "nix/store/globals.hh" +#include "nix/util/compression.hh" +#include "nix/store/derivations.hh" namespace nix { @@ -22,8 +22,9 @@ LocalFSStoreConfig::LocalFSStoreConfig(PathView rootDir, const Params & params) { } -LocalFSStore::LocalFSStore(const Params & params) - : Store(params) +LocalFSStore::LocalFSStore(const Config & config) + : Store{static_cast(*this)} + , config{config} { } @@ -33,30 +34,35 @@ struct LocalStoreAccessor : PosixSourceAccessor bool requireValidPath; LocalStoreAccessor(ref store, bool requireValidPath) - : store(store) + : PosixSourceAccessor(std::filesystem::path{store->config.realStoreDir.get()}) + , store(store) , requireValidPath(requireValidPath) - { } + { + } + - CanonPath toRealPath(const CanonPath & path) + void requireStoreObject(const CanonPath & path) { - auto [storePath, rest] = store->toStorePath(path.abs()); + auto [storePath, rest] = store->toStorePath(store->storeDir + path.abs()); if (requireValidPath && !store->isValidPath(storePath)) throw InvalidPath("path '%1%' is not a valid store path", store->printStorePath(storePath)); - return CanonPath(store->getRealStoreDir()) / storePath.to_string() / CanonPath(rest); } std::optional maybeLstat(const CanonPath & path) override { - /* Handle the case where `path` is (a parent of) the store. */ - if (isDirOrInDir(store->storeDir, path.abs())) + /* Also allow `path` to point to the entire store, which is + needed for resolving symlinks. */ + if (path.isRoot()) return Stat{ .type = tDirectory }; - return PosixSourceAccessor::maybeLstat(toRealPath(path)); + requireStoreObject(path); + return PosixSourceAccessor::maybeLstat(path); } DirEntries readDirectory(const CanonPath & path) override { - return PosixSourceAccessor::readDirectory(toRealPath(path)); + requireStoreObject(path); + return PosixSourceAccessor::readDirectory(path); } void readFile( @@ -64,12 +70,14 @@ struct LocalStoreAccessor : PosixSourceAccessor Sink & sink, std::function sizeCallback) override { - return PosixSourceAccessor::readFile(toRealPath(path), sink, sizeCallback); + requireStoreObject(path); + return PosixSourceAccessor::readFile(path, sink, sizeCallback); } std::string readLink(const CanonPath & path) override { - return PosixSourceAccessor::readLink(toRealPath(path)); + requireStoreObject(path); + return PosixSourceAccessor::readLink(path); } }; @@ -97,8 +105,8 @@ std::optional LocalFSStore::getBuildLogExact(const StorePath & path Path logPath = j == 0 - ? fmt("%s/%s/%s/%s", logDir, drvsLogDir, baseName.substr(0, 2), baseName.substr(2)) - : fmt("%s/%s/%s", logDir, drvsLogDir, baseName); + ? fmt("%s/%s/%s/%s", config.logDir.get(), drvsLogDir, baseName.substr(0, 2), baseName.substr(2)) + : fmt("%s/%s/%s", config.logDir.get(), drvsLogDir, baseName); Path logBz2Path = logPath + ".bz2"; if (pathExists(logPath)) diff --git a/src/libstore/local-overlay-store.cc b/src/libstore/local-overlay-store.cc index 56ff6bef3e5..e40c5fa6e6a 100644 --- a/src/libstore/local-overlay-store.cc +++ b/src/libstore/local-overlay-store.cc @@ -1,10 +1,13 @@ -#include "local-overlay-store.hh" -#include "callback.hh" -#include "realisation.hh" -#include "processes.hh" -#include "url.hh" #include +#include "nix/store/local-overlay-store.hh" +#include "nix/util/callback.hh" +#include "nix/store/realisation.hh" +#include "nix/util/processes.hh" +#include "nix/util/url.hh" +#include "nix/store/store-open.hh" +#include "nix/store/store-registration.hh" + namespace nix { std::string LocalOverlayStoreConfig::doc() @@ -14,25 +17,32 @@ std::string LocalOverlayStoreConfig::doc() ; } -Path LocalOverlayStoreConfig::toUpperPath(const StorePath & path) { +ref LocalOverlayStoreConfig::openStore() const +{ + return make_ref(ref{ + std::dynamic_pointer_cast(shared_from_this()) + }); +} + + +Path LocalOverlayStoreConfig::toUpperPath(const StorePath & path) const +{ return upperLayer + "/" + path.to_string(); } -LocalOverlayStore::LocalOverlayStore(std::string_view scheme, PathView path, const Params & params) - : StoreConfig(params) - , LocalFSStoreConfig(path, params) - , LocalStoreConfig(params) - , LocalOverlayStoreConfig(scheme, path, params) - , Store(params) - , LocalFSStore(params) - , LocalStore(params) - , lowerStore(openStore(percentDecode(lowerStoreUri.get())).dynamic_pointer_cast()) + +LocalOverlayStore::LocalOverlayStore(ref config) + : Store{*config} + , LocalFSStore{*config} + , LocalStore{static_cast>(config)} + , config{config} + , lowerStore(openStore(percentDecode(config->lowerStoreUri.get())).dynamic_pointer_cast()) { - if (checkMount.get()) { + if (config->checkMount.get()) { std::smatch match; std::string mountInfo; auto mounts = readFile(std::filesystem::path{"/proc/self/mounts"}); - auto regex = std::regex(R"((^|\n)overlay )" + realStoreDir.get() + R"( .*(\n|$))"); + auto regex = std::regex(R"((^|\n)overlay )" + config->realStoreDir.get() + R"( .*(\n|$))"); // Mount points can be stacked, so there might be multiple matching entries. // Loop until the last match, which will be the current state of the mount point. @@ -45,13 +55,13 @@ LocalOverlayStore::LocalOverlayStore(std::string_view scheme, PathView path, con return std::regex_search(mountInfo, std::regex("\\b" + option + "=" + value + "( |,)")); }; - auto expectedLowerDir = lowerStore->realStoreDir.get(); - if (!checkOption("lowerdir", expectedLowerDir) || !checkOption("upperdir", upperLayer)) { + auto expectedLowerDir = lowerStore->config.realStoreDir.get(); + if (!checkOption("lowerdir", expectedLowerDir) || !checkOption("upperdir", config->upperLayer)) { debug("expected lowerdir: %s", expectedLowerDir); - debug("expected upperdir: %s", upperLayer); + debug("expected upperdir: %s", config->upperLayer); debug("actual mount: %s", mountInfo); throw Error("overlay filesystem '%s' mounted incorrectly", - realStoreDir.get()); + config->realStoreDir.get()); } } } @@ -201,14 +211,14 @@ void LocalOverlayStore::collectGarbage(const GCOptions & options, GCResults & re void LocalOverlayStore::deleteStorePath(const Path & path, uint64_t & bytesFreed) { - auto mergedDir = realStoreDir.get() + "/"; + auto mergedDir = config->realStoreDir.get() + "/"; if (path.substr(0, mergedDir.length()) != mergedDir) { warn("local-overlay: unexpected gc path '%s' ", path); return; } StorePath storePath = {path.substr(mergedDir.length())}; - auto upperPath = toUpperPath(storePath); + auto upperPath = config->toUpperPath(storePath); if (pathExists(upperPath)) { debug("upper exists: %s", path); @@ -257,7 +267,7 @@ LocalStore::VerificationResult LocalOverlayStore::verifyAllValidPaths(RepairFlag StorePathSet done; auto existsInStoreDir = [&](const StorePath & storePath) { - return pathExists(realStoreDir.get() + "/" + storePath.to_string()); + return pathExists(config->realStoreDir.get() + "/" + storePath.to_string()); }; bool errors = false; @@ -277,16 +287,16 @@ void LocalOverlayStore::remountIfNecessary() { if (!_remountRequired) return; - if (remountHook.get().empty()) { - warn("'%s' needs remounting, set remount-hook to do this automatically", realStoreDir.get()); + if (config->remountHook.get().empty()) { + warn("'%s' needs remounting, set remount-hook to do this automatically", config->realStoreDir.get()); } else { - runProgram(remountHook, false, {realStoreDir}); + runProgram(config->remountHook, false, {config->realStoreDir}); } _remountRequired = false; } -static RegisterStoreImplementation regLocalOverlayStore; +static RegisterStoreImplementation regLocalOverlayStore; } diff --git a/src/libstore/local-overlay-store.md b/src/libstore/local-overlay-store.md index 9434ebfb95c..4ac8a476d1b 100644 --- a/src/libstore/local-overlay-store.md +++ b/src/libstore/local-overlay-store.md @@ -85,7 +85,7 @@ The parts of a local overlay store are as follows: > The location of the database instead depends on the [`state`](#store-experimental-local-overlay-store-state) setting. > It is always `${state}/db`. - This contains the metadata of all of the upper layer [store objects][store object] (everything beyond their file system objects), and also duplicate copies of some lower layer store object's metadta. + This contains the metadata of all of the upper layer [store objects][store object] (everything beyond their file system objects), and also duplicate copies of some lower layer store object's metadata. The duplication is so the metadata for the [closure](@docroot@/glossary.md#gloss-closure) of upper layer [store objects][store object] can be found entirely within the upper layer. (This allows us to use the same SQL Schema as the [local store]'s SQLite database, as foreign keys in that schema enforce closure metadata to be self-contained in this way.) diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index f708bd1b008..0d2d96e6119 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -1,22 +1,24 @@ -#include "local-store.hh" -#include "globals.hh" -#include "git.hh" -#include "archive.hh" -#include "pathlocks.hh" -#include "worker-protocol.hh" -#include "derivations.hh" -#include "realisation.hh" -#include "nar-info.hh" -#include "references.hh" -#include "callback.hh" -#include "topo-sort.hh" -#include "finally.hh" -#include "compression.hh" -#include "signals.hh" -#include "posix-fs-canonicalise.hh" -#include "posix-source-accessor.hh" -#include "keys.hh" -#include "users.hh" +#include "nix/store/local-store.hh" +#include "nix/store/globals.hh" +#include "nix/util/git.hh" +#include "nix/util/archive.hh" +#include "nix/store/pathlocks.hh" +#include "nix/store/worker-protocol.hh" +#include "nix/store/derivations.hh" +#include "nix/store/realisation.hh" +#include "nix/store/nar-info.hh" +#include "nix/util/references.hh" +#include "nix/util/callback.hh" +#include "nix/util/topo-sort.hh" +#include "nix/util/finally.hh" +#include "nix/util/compression.hh" +#include "nix/util/signals.hh" +#include "nix/store/posix-fs-canonicalise.hh" +#include "nix/util/posix-source-accessor.hh" +#include "nix/store/keys.hh" +#include "nix/util/users.hh" +#include "nix/store/store-open.hh" +#include "nix/store/store-registration.hh" #include #include @@ -38,7 +40,7 @@ # include #endif -#if __linux__ +#ifdef __linux__ # include # include # include @@ -52,7 +54,9 @@ #include -#include "strings.hh" +#include "nix/util/strings.hh" + +#include "store-config-private.hh" namespace nix { @@ -73,6 +77,21 @@ std::string LocalStoreConfig::doc() ; } +Path LocalBuildStoreConfig::getBuildDir() const +{ + return + settings.buildDir.get().has_value() + ? *settings.buildDir.get() + : buildDir.get().has_value() + ? *buildDir.get() + : stateDir.get() + "/builds"; +} + +ref LocalStore::Config::openStore() const +{ + return make_ref(ref{shared_from_this()}); +} + struct LocalStore::State::Stmts { /* Some precompiled SQLite statements. */ SQLiteStmt RegisterValidPath; @@ -95,49 +114,45 @@ struct LocalStore::State::Stmts { SQLiteStmt AddRealisationReference; }; -LocalStore::LocalStore( - std::string_view scheme, - PathView path, - const Params & params) - : StoreConfig(params) - , LocalFSStoreConfig(path, params) - , LocalStoreConfig(scheme, path, params) - , Store(params) - , LocalFSStore(params) - , dbDir(stateDir + "/db") - , linksDir(realStoreDir + "/.links") +LocalStore::LocalStore(ref config) + : Store{*config} + , LocalFSStore{*config} + , config{config} + , dbDir(config->stateDir + "/db") + , linksDir(config->realStoreDir + "/.links") , reservedPath(dbDir + "/reserved") , schemaPath(dbDir + "/schema") - , tempRootsDir(stateDir + "/temproots") + , tempRootsDir(config->stateDir + "/temproots") , fnTempRoots(fmt("%s/%d", tempRootsDir, getpid())) - , locksHeld(tokenizeString(getEnv("NIX_HELD_LOCKS").value_or(""))) { auto state(_state.lock()); state->stmts = std::make_unique(); /* Create missing state directories if they don't already exist. */ - createDirs(realStoreDir); - if (readOnly) { + createDirs(config->realStoreDir.get()); + if (config->readOnly) { experimentalFeatureSettings.require(Xp::ReadOnlyLocalStore); } else { makeStoreWritable(); } createDirs(linksDir); - Path profilesDir = stateDir + "/profiles"; + Path profilesDir = config->stateDir + "/profiles"; createDirs(profilesDir); createDirs(tempRootsDir); createDirs(dbDir); - Path gcRootsDir = stateDir + "/gcroots"; + Path gcRootsDir = config->stateDir + "/gcroots"; if (!pathExists(gcRootsDir)) { createDirs(gcRootsDir); - createSymlink(profilesDir, gcRootsDir + "/profiles"); + replaceSymlink(profilesDir, gcRootsDir + "/profiles"); } for (auto & perUserDir : {profilesDir + "/per-user", gcRootsDir + "/per-user"}) { createDirs(perUserDir); - if (!readOnly) { - if (chmod(perUserDir.c_str(), 0755) == -1) - throw SysError("could not set permissions on '%s' to 755", perUserDir); + if (!config->readOnly) { + // Skip chmod call if the directory already has the correct permissions (0755). + // This is to avoid failing when the executing user lacks permissions to change the directory's permissions + // even if it would be no-op. + chmodIfNeeded(perUserDir, 0755, S_IRWXU | S_IRWXG | S_IRWXO); } } @@ -150,16 +165,16 @@ LocalStore::LocalStore( struct group * gr = getgrnam(settings.buildUsersGroup.get().c_str()); if (!gr) printError("warning: the group '%1%' specified in 'build-users-group' does not exist", settings.buildUsersGroup); - else if (!readOnly) { + else if (!config->readOnly) { struct stat st; - if (stat(realStoreDir.get().c_str(), &st)) - throw SysError("getting attributes of path '%1%'", realStoreDir); + if (stat(config->realStoreDir.get().c_str(), &st)) + throw SysError("getting attributes of path '%1%'", config->realStoreDir); if (st.st_uid != 0 || st.st_gid != gr->gr_gid || (st.st_mode & ~S_IFMT) != perm) { - if (chown(realStoreDir.get().c_str(), 0, gr->gr_gid) == -1) - throw SysError("changing ownership of path '%1%'", realStoreDir); - if (chmod(realStoreDir.get().c_str(), perm) == -1) - throw SysError("changing permissions on path '%1%'", realStoreDir); + if (chown(config->realStoreDir.get().c_str(), 0, gr->gr_gid) == -1) + throw SysError("changing ownership of path '%1%'", config->realStoreDir); + if (chmod(config->realStoreDir.get().c_str(), perm) == -1) + throw SysError("changing permissions on path '%1%'", config->realStoreDir); } } } @@ -167,16 +182,15 @@ LocalStore::LocalStore( /* Ensure that the store and its parents are not symlinks. */ if (!settings.allowSymlinkedStore) { - Path path = realStoreDir; - struct stat st; - while (path != "/") { - st = lstat(path); - if (S_ISLNK(st.st_mode)) + std::filesystem::path path = config->realStoreDir.get(); + std::filesystem::path root = path.root_path(); + while (path != root) { + if (std::filesystem::is_symlink(path)) throw Error( "the path '%1%' is a symlink; " "this is not allowed for the Nix store and its parent directories", path); - path = dirOf(path); + path = path.parent_path(); } } @@ -215,12 +229,12 @@ LocalStore::LocalStore( /* Acquire the big fat lock in shared mode to make sure that no schema upgrade is in progress. */ - if (!readOnly) { + if (!config->readOnly) { Path globalLockPath = dbDir + "/big-lock"; globalLock = openLockFile(globalLockPath.c_str(), true); } - if (!readOnly && !lockFile(globalLock.get(), ltRead, false)) { + if (!config->readOnly && !lockFile(globalLock.get(), ltRead, false)) { printInfo("waiting for the big Nix store lock..."); lockFile(globalLock.get(), ltRead, true); } @@ -228,7 +242,7 @@ LocalStore::LocalStore( /* Check the current database schema and if necessary do an upgrade. */ int curSchema = getSchema(); - if (readOnly && curSchema < nixSchemaVersion) { + if (config->readOnly && curSchema < nixSchemaVersion) { debug("current schema version: %d", curSchema); debug("supported schema version: %d", nixSchemaVersion); throw Error(curSchema == 0 ? @@ -243,7 +257,7 @@ LocalStore::LocalStore( else if (curSchema == 0) { /* new store */ curSchema = nixSchemaVersion; openDB(*state, true); - writeFile(schemaPath, fmt("%1%", nixSchemaVersion), 0666, true); + writeFile(schemaPath, fmt("%1%", curSchema), 0666, FsSync::Yes); } else if (curSchema < nixSchemaVersion) { @@ -294,7 +308,7 @@ LocalStore::LocalStore( txn.commit(); } - writeFile(schemaPath, fmt("%1%", nixSchemaVersion), 0666, true); + writeFile(schemaPath, fmt("%1%", nixSchemaVersion), 0666, FsSync::Yes); lockFile(globalLock.get(), ltRead, true); } @@ -376,15 +390,9 @@ LocalStore::LocalStore( } -LocalStore::LocalStore(const Params & params) - : LocalStore("local", "", params) -{ -} - - AutoCloseFD LocalStore::openGCLock() { - Path fnGCLock = stateDir + "/gc.lock"; + Path fnGCLock = config->stateDir + "/gc.lock"; auto fdGCLock = open(fnGCLock.c_str(), O_RDWR | O_CREAT #ifndef _WIN32 | O_CLOEXEC @@ -450,17 +458,17 @@ int LocalStore::getSchema() void LocalStore::openDB(State & state, bool create) { - if (create && readOnly) { + if (create && config->readOnly) { throw Error("cannot create database while in read-only mode"); } - if (access(dbDir.c_str(), R_OK | (readOnly ? 0 : W_OK))) + if (access(dbDir.c_str(), R_OK | (config->readOnly ? 0 : W_OK))) throw SysError("Nix database directory '%1%' is not writable", dbDir); /* Open the Nix database. */ std::string dbPath = dbDir + "/db.sqlite"; auto & db(state.db); - auto openMode = readOnly ? SQLiteOpenMode::Immutable + auto openMode = config->readOnly ? SQLiteOpenMode::Immutable : create ? SQLiteOpenMode::Normal : SQLiteOpenMode::NoCreate; state.db = SQLite(dbPath, openMode); @@ -533,7 +541,7 @@ void LocalStore::upgradeDBSchema(State & state) { state.db.exec("create table if not exists SchemaMigrations (migration text primary key not null);"); - std::set schemaMigrations; + StringSet schemaMigrations; { SQLiteStmt querySchemaMigrations; @@ -569,16 +577,16 @@ void LocalStore::upgradeDBSchema(State & state) bind mount. So make the Nix store writable for this process. */ void LocalStore::makeStoreWritable() { -#if __linux__ +#ifdef __linux__ if (!isRootUser()) return; /* Check if /nix/store is on a read-only mount. */ struct statvfs stat; - if (statvfs(realStoreDir.get().c_str(), &stat) != 0) + if (statvfs(config->realStoreDir.get().c_str(), &stat) != 0) throw SysError("getting info about the Nix store mount point"); if (stat.f_flag & ST_RDONLY) { - if (mount(0, realStoreDir.get().c_str(), "none", MS_REMOUNT | MS_BIND, 0) == -1) - throw SysError("remounting %1% writable", realStoreDir); + if (mount(0, config->realStoreDir.get().c_str(), "none", MS_REMOUNT | MS_BIND, 0) == -1) + throw SysError("remounting %1% writable", config->realStoreDir); } #endif } @@ -918,7 +926,7 @@ StorePathSet LocalStore::querySubstitutablePaths(const StorePathSet & paths) for (auto & sub : getDefaultSubstituters()) { if (remaining.empty()) break; if (sub->storeDir != storeDir) continue; - if (!sub->wantMassQuery) continue; + if (!sub->config.wantMassQuery) continue; auto valid = sub->queryValidPaths(remaining); @@ -1030,12 +1038,12 @@ const PublicKeys & LocalStore::getPublicKeys() bool LocalStore::pathInfoIsUntrusted(const ValidPathInfo & info) { - return requireSigs && !info.checkSignatures(*this, getPublicKeys()); + return config->requireSigs && !info.checkSignatures(*this, getPublicKeys()); } bool LocalStore::realisationIsUntrusted(const Realisation & realisation) { - return requireSigs && !realisation.checkSignatures(getPublicKeys()); + return config->requireSigs && !realisation.checkSignatures(getPublicKeys()); } void LocalStore::addToStore(const ValidPathInfo & info, Source & source, @@ -1100,7 +1108,7 @@ void LocalStore::addToStore(const ValidPathInfo & info, Source & source, auto & specified = *info.ca; auto actualHash = ({ auto accessor = getFSAccessor(false); - CanonPath path { printStorePath(info.path) }; + CanonPath path { info.path.to_string() }; Hash h { HashAlgorithm::SHA256 }; // throwaway def to appease C++ auto fim = specified.method.getFileIngestionMethod(); switch (fim) { @@ -1332,7 +1340,7 @@ std::pair LocalStore::createTempDirInStore() /* There is a slight possibility that `tmpDir' gets deleted by the GC between createTempDir() and when we acquire a lock on it. We'll repeat until 'tmpDir' exists and we've locked it. */ - tmpDirFn = createTempDir(realStoreDir, "tmp"); + tmpDirFn = createTempDir(config->realStoreDir, "tmp"); tmpDirFd = openDirectory(tmpDirFn); if (!tmpDirFd) { continue; @@ -1380,7 +1388,7 @@ bool LocalStore::verifyStore(bool checkContents, RepairFlag repair) printInfo("checking link hashes..."); - for (auto & link : std::filesystem::directory_iterator{linksDir}) { + for (auto & link : DirectoryIterator{linksDir}) { checkInterrupt(); auto name = link.path().filename(); printMsg(lvlTalkative, "checking contents of '%s'", name); @@ -1473,7 +1481,7 @@ LocalStore::VerificationResult LocalStore::verifyAllValidPaths(RepairFlag repair database and the filesystem) in the loop below, in order to catch invalid states. */ - for (auto & i : std::filesystem::directory_iterator{realStoreDir.to_string()}) { + for (auto & i : DirectoryIterator{config->realStoreDir.get()}) { checkInterrupt(); try { storePathsInStoreDir.insert({i.path().filename().string()}); @@ -1582,33 +1590,6 @@ void LocalStore::addSignatures(const StorePath & storePath, const StringSet & si } -void LocalStore::signRealisation(Realisation & realisation) -{ - // FIXME: keep secret keys in memory. - - auto secretKeyFiles = settings.secretKeyFiles; - - for (auto & secretKeyFile : secretKeyFiles.get()) { - SecretKey secretKey(readFile(secretKeyFile)); - LocalSigner signer(std::move(secretKey)); - realisation.sign(signer); - } -} - -void LocalStore::signPathInfo(ValidPathInfo & info) -{ - // FIXME: keep secret keys in memory. - - auto secretKeyFiles = settings.secretKeyFiles; - - for (auto & secretKeyFile : secretKeyFiles.get()) { - SecretKey secretKey(readFile(secretKeyFile)); - LocalSigner signer(std::move(secretKey)); - info.sign(*this, signer); - } -} - - std::optional> LocalStore::queryRealisationCore_( LocalStore::State & state, const DrvOutput & id) @@ -1689,7 +1670,7 @@ void LocalStore::addBuildLog(const StorePath & drvPath, std::string_view log) auto baseName = drvPath.to_string(); - auto logPath = fmt("%s/%s/%s/%s.bz2", logDir, drvsLogDir, baseName.substr(0, 2), baseName.substr(2)); + auto logPath = fmt("%s/%s/%s/%s.bz2", config->logDir, drvsLogDir, baseName.substr(0, 2), baseName.substr(2)); if (pathExists(logPath)) return; @@ -1707,6 +1688,6 @@ std::optional LocalStore::getVersion() return nixVersion; } -static RegisterStoreImplementation regLocalStore; +static RegisterStoreImplementation regLocalStore; } // namespace nix diff --git a/src/libstore/log-store.cc b/src/libstore/log-store.cc index 8a26832ab28..2ef791e19a0 100644 --- a/src/libstore/log-store.cc +++ b/src/libstore/log-store.cc @@ -1,4 +1,4 @@ -#include "log-store.hh" +#include "nix/store/log-store.hh" namespace nix { diff --git a/src/libstore/machines.cc b/src/libstore/machines.cc index eb729b697f1..483b337bf21 100644 --- a/src/libstore/machines.cc +++ b/src/libstore/machines.cc @@ -1,6 +1,6 @@ -#include "machines.hh" -#include "globals.hh" -#include "store-api.hh" +#include "nix/store/machines.hh" +#include "nix/store/globals.hh" +#include "nix/store/store-open.hh" #include @@ -47,7 +47,7 @@ bool Machine::systemSupported(const std::string & system) const return system == "builtin" || (systemTypes.count(system) > 0); } -bool Machine::allSupported(const std::set & features) const +bool Machine::allSupported(const StringSet & features) const { return std::all_of(features.begin(), features.end(), [&](const std::string & feature) { @@ -56,7 +56,7 @@ bool Machine::allSupported(const std::set & features) const }); } -bool Machine::mandatoryMet(const std::set & features) const +bool Machine::mandatoryMet(const StringSet & features) const { return std::all_of(mandatoryFeatures.begin(), mandatoryFeatures.end(), [&](const std::string & feature) { @@ -105,33 +105,36 @@ ref Machine::openStore() const static std::vector expandBuilderLines(const std::string & builders) { std::vector result; - for (auto line : tokenizeString>(builders, "\n;")) { - trim(line); + for (auto line : tokenizeString>(builders, "\n")) { line.erase(std::find(line.begin(), line.end(), '#'), line.end()); - if (line.empty()) continue; - - if (line[0] == '@') { - const std::string path = trim(std::string(line, 1)); - std::string text; - try { - text = readFile(path); - } catch (const SysError & e) { - if (e.errNo != ENOENT) - throw; - debug("cannot find machines file '%s'", path); + for (auto entry : tokenizeString>(line, ";")) { + entry = trim(entry); + + if (entry.empty()) { + // skip blank entries + } else if (entry[0] == '@') { + const std::string path = trim(std::string_view{entry}.substr(1)); + std::string text; + try { + text = readFile(path); + } catch (const SysError & e) { + if (e.errNo != ENOENT) + throw; + debug("cannot find machines file '%s'", path); + continue; + } + + const auto entrys = expandBuilderLines(text); + result.insert(end(result), begin(entrys), end(entrys)); + } else { + result.emplace_back(entry); } - - const auto lines = expandBuilderLines(text); - result.insert(end(result), begin(lines), end(lines)); - continue; } - - result.emplace_back(line); } return result; } -static Machine parseBuilderLine(const std::set & defaultSystems, const std::string & line) +static Machine parseBuilderLine(const StringSet & defaultSystems, const std::string & line) { const auto tokens = tokenizeString>(line); @@ -175,7 +178,7 @@ static Machine parseBuilderLine(const std::set & defaultSystems, co // `storeUri` tokens[0], // `systemTypes` - isSet(1) ? tokenizeString>(tokens[1], ",") : defaultSystems, + isSet(1) ? tokenizeString(tokens[1], ",") : defaultSystems, // `sshKey` isSet(2) ? tokens[2] : "", // `maxJobs` @@ -183,15 +186,15 @@ static Machine parseBuilderLine(const std::set & defaultSystems, co // `speedFactor` isSet(4) ? parseFloatField(4) : 1.0f, // `supportedFeatures` - isSet(5) ? tokenizeString>(tokens[5], ",") : std::set{}, + isSet(5) ? tokenizeString(tokens[5], ",") : StringSet{}, // `mandatoryFeatures` - isSet(6) ? tokenizeString>(tokens[6], ",") : std::set{}, + isSet(6) ? tokenizeString(tokens[6], ",") : StringSet{}, // `sshPublicHostKey` isSet(7) ? ensureBase64(7) : "" }; } -static Machines parseBuilderLines(const std::set & defaultSystems, const std::vector & builders) +static Machines parseBuilderLines(const StringSet & defaultSystems, const std::vector & builders) { Machines result; std::transform( @@ -200,7 +203,7 @@ static Machines parseBuilderLines(const std::set & defaultSystems, return result; } -Machines Machine::parseConfig(const std::set & defaultSystems, const std::string & s) +Machines Machine::parseConfig(const StringSet & defaultSystems, const std::string & s) { const auto builderLines = expandBuilderLines(s); return parseBuilderLines(defaultSystems, builderLines); diff --git a/src/libstore/make-content-addressed.cc b/src/libstore/make-content-addressed.cc index a3130d7cc02..606d72866c6 100644 --- a/src/libstore/make-content-addressed.cc +++ b/src/libstore/make-content-addressed.cc @@ -1,5 +1,5 @@ -#include "make-content-addressed.hh" -#include "references.hh" +#include "nix/store/make-content-addressed.hh" +#include "nix/util/references.hh" namespace nix { diff --git a/src/libstore/meson.build b/src/libstore/meson.build index f836b8d4f62..d82bcddc14c 100644 --- a/src/libstore/meson.build +++ b/src/libstore/meson.build @@ -4,8 +4,6 @@ project('nix-store', 'cpp', 'cpp_std=c++2a', # TODO(Qyriad): increase the warning level 'warning_level=1', - 'debug=true', - 'optimization=2', 'errorlogs=true', # Please print logs for tests that fail 'localstatedir=/nix/var', ], @@ -15,21 +13,29 @@ project('nix-store', 'cpp', cxx = meson.get_compiler('cpp') -subdir('build-utils-meson/deps-lists') +subdir('nix-meson-build-support/deps-lists') -configdata = configuration_data() +configdata_pub = configuration_data() +configdata_priv = configuration_data() # TODO rename, because it will conflict with downstream projects -configdata.set_quoted('PACKAGE_VERSION', meson.project_version()) - -configdata.set_quoted('SYSTEM', host_machine.cpu_family() + '-' + host_machine.system()) +configdata_priv.set_quoted('PACKAGE_VERSION', meson.project_version()) + +# Used in public header. +configdata_pub.set_quoted( + 'NIX_LOCAL_SYSTEM', + host_machine.cpu_family() + '-' + host_machine.system(), + description : + 'This is the system name Nix expects for local running instance of Nix.\n\n' + + 'See the "system" setting for additional details', +) deps_private_maybe_subproject = [ ] deps_public_maybe_subproject = [ dependency('nix-util'), ] -subdir('build-utils-meson/subprojects') +subdir('nix-meson-build-support/subprojects') run_command('ln', '-s', meson.project_build_root() / '__nothing_link_target', @@ -49,27 +55,30 @@ run_command('rm', '-f', check : true, ) summary('can hardlink to symlink', can_link_symlink, bool_yn : true) -configdata.set('CAN_LINK_SYMLINK', can_link_symlink.to_int()) +configdata_priv.set('CAN_LINK_SYMLINK', can_link_symlink.to_int()) # Check for each of these functions, and create a define like `#define HAVE_LCHOWN 1`. -# -# Only need to do functions that deps (like `libnixutil`) didn't already -# check for. check_funcs = [ # Optionally used for canonicalising files from the build 'lchown', + 'posix_fallocate', 'statvfs', ] foreach funcspec : check_funcs define_name = 'HAVE_' + funcspec.underscorify().to_upper() define_value = cxx.has_function(funcspec).to_int() - configdata.set(define_name, define_value) + configdata_priv.set(define_name, define_value) endforeach has_acl_support = cxx.has_header('sys/xattr.h') \ and cxx.has_function('llistxattr') \ and cxx.has_function('lremovexattr') -configdata.set('HAVE_ACL_SUPPORT', has_acl_support.to_int()) +# Used in public header. Affects ABI! +configdata_pub.set( + 'NIX_SUPPORT_ACL', + has_acl_support.to_int(), + description : 'FIXME: It\'s a bit peculiar that this needs to be exposed. The reason is that that it effects whether the settings struct in a header has a particular field. This is also odd, because it means when there is no ACL support one will just get an "unknown setting" warning from their configuration.', +) if host_machine.system() == 'darwin' sandbox = cxx.find_library('sandbox') @@ -81,11 +90,11 @@ if host_machine.system() == 'windows' deps_other += [wsock32] endif -subdir('build-utils-meson/libatomic') +subdir('nix-meson-build-support/libatomic') boost = dependency( 'boost', - modules : ['container'], + modules : ['container', 'regex'], include_type: 'system', ) # boost is a public dependency, but not a pkg-config dependency unfortunately, so we @@ -105,7 +114,7 @@ seccomp = dependency('libseccomp', 'seccomp', required : seccomp_required, versi if is_linux and not seccomp.found() warning('Sandbox security is reduced because libseccomp has not been found! Please provide libseccomp if it supports your CPU architecture.') endif -configdata.set('HAVE_SECCOMP', seccomp.found().to_int()) +configdata_priv.set('HAVE_SECCOMP', seccomp.found().to_int()) deps_private += seccomp nlohmann_json = dependency('nlohmann_json', version : '>= 3.9') @@ -114,9 +123,11 @@ deps_public += nlohmann_json sqlite = dependency('sqlite3', 'sqlite', version : '>=3.6.19') deps_private += sqlite -# AWS C++ SDK has bad pkg-config +# AWS C++ SDK has bad pkg-config. See +# https://github.com/aws/aws-sdk-cpp/issues/2673 for details. aws_s3 = dependency('aws-cpp-sdk-s3', required : false) -configdata.set('ENABLE_S3', aws_s3.found().to_int()) +# The S3 store definitions in the header will be hidden based on this variables. +configdata_pub.set('NIX_WITH_S3_SUPPORT', aws_s3.found().to_int()) if aws_s3.found() aws_s3 = declare_dependency( include_directories: include_directories(aws_s3.get_variable('includedir')), @@ -124,6 +135,9 @@ if aws_s3.found() '-L' + aws_s3.get_variable('libdir'), '-laws-cpp-sdk-transfer', '-laws-cpp-sdk-s3', + '-laws-cpp-sdk-identity-management', + '-laws-cpp-sdk-cognito-identity', + '-laws-cpp-sdk-sts', '-laws-cpp-sdk-core', '-laws-crt-cpp', ], @@ -131,7 +145,7 @@ if aws_s3.found() endif deps_other += aws_s3 -subdir('build-utils-meson/generate-header') +subdir('nix-meson-build-support/generate-header') generated_headers = [] foreach header : [ @@ -143,12 +157,15 @@ endforeach busybox = find_program(get_option('sandbox-shell'), required : false) +configdata_priv.set('HAVE_EMBEDDED_SANDBOX_SHELL', get_option('embedded-sandbox-shell').to_int()) + +if get_option('embedded-sandbox-shell') + configdata_priv.set_quoted('SANDBOX_SHELL', '__embedded_sandbox_shell__') +elif busybox.found() + configdata_priv.set_quoted('SANDBOX_SHELL', busybox.full_path()) +endif + if get_option('embedded-sandbox-shell') - # This one goes in config.h - # The path to busybox is passed as a -D flag when compiling this_library. - # This solution is inherited from the old make buildsystem - # TODO: do this differently? - configdata.set('HAVE_EMBEDDED_SANDBOX_SHELL', 1) hexdump = find_program('hexdump', native : true) embedded_sandbox_shell_gen = custom_target( 'embedded-sandbox-shell.gen.hh', @@ -166,25 +183,79 @@ if get_option('embedded-sandbox-shell') generated_headers += embedded_sandbox_shell_gen endif -config_h = configure_file( - configuration : configdata, - output : 'config-store.hh', +fs = import('fs') + +prefix = get_option('prefix') +# For each of these paths, assume that it is relative to the prefix unless +# it is already an absolute path (which is the default for store-dir, localstatedir, and log-dir). +path_opts = [ + # Meson built-ins. + 'datadir', + 'mandir', + 'libdir', + 'includedir', + 'libexecdir', + # Homecooked Nix directories. + 'store-dir', + 'localstatedir', + 'log-dir', +] +# For your grepping pleasure, this loop sets the following variables that aren't mentioned +# literally above: +# store_dir +# localstatedir +# log_dir +# profile_dir +foreach optname : path_opts + varname = optname.replace('-', '_') + path = get_option(optname) + if fs.is_absolute(path) + set_variable(varname, path) + else + set_variable(varname, prefix / path) + endif +endforeach + +# sysconfdir doesn't get anything installed to directly, and is only used to +# tell Nix where to look for nix.conf, so it doesn't get appended to prefix. +sysconfdir = get_option('sysconfdir') +if not fs.is_absolute(sysconfdir) + sysconfdir = '/' / sysconfdir +endif + +# Aside from prefix itself, each of these was made into an absolute path +# by joining it with prefix, unless it was already an absolute path +# (which is the default for store-dir, localstatedir, and log-dir). +configdata_priv.set_quoted('NIX_PREFIX', prefix) +configdata_priv.set_quoted('NIX_STORE_DIR', store_dir) +configdata_priv.set_quoted('NIX_DATA_DIR', datadir) +configdata_priv.set_quoted('NIX_STATE_DIR', localstatedir / 'nix') +configdata_priv.set_quoted('NIX_LOG_DIR', log_dir) +configdata_priv.set_quoted('NIX_CONF_DIR', sysconfdir / 'nix') +configdata_priv.set_quoted('NIX_MAN_DIR', mandir) + +lsof = find_program('lsof', required : false) +configdata_priv.set_quoted( + 'LSOF', + lsof.found() + ? lsof.full_path() + # Just look up on the PATH + : 'lsof', ) -add_project_arguments( - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. - '-include', 'config-util.hh', - '-include', 'config-store.hh', - language : 'cpp', +config_priv_h = configure_file( + configuration : configdata_priv, + output : 'store-config-private.hh', ) -subdir('build-utils-meson/common') +subdir('nix-meson-build-support/common') sources = files( 'binary-cache-store.cc', 'build-result.cc', 'build/derivation-goal.cc', + 'build/derivation-building-goal.cc', + 'build/derivation-trampoline-goal.cc', 'build/drv-output-substitution-goal.cc', 'build/entry-points.cc', 'build/goal.cc', @@ -198,6 +269,7 @@ sources = files( 'content-address.cc', 'daemon.cc', 'derivations.cc', + 'derivation-options.cc', 'derived-path-map.cc', 'derived-path.cc', 'downstream-placeholder.cc', @@ -235,6 +307,7 @@ sources = files( 'realisation.cc', 'remote-fs-accessor.cc', 'remote-store.cc', + 'restricted-store.cc', 's3-binary-cache-store.cc', 'serve-protocol-connection.cc', 'serve-protocol.cc', @@ -242,86 +315,15 @@ sources = files( 'ssh-store.cc', 'ssh.cc', 'store-api.cc', + 'store-dir-config.cc', + 'store-registration.cc', 'store-reference.cc', 'uds-remote-store.cc', 'worker-protocol-connection.cc', 'worker-protocol.cc', ) -include_dirs = [ - include_directories('.'), - include_directories('build'), -] - -headers = [config_h] + files( - 'binary-cache-store.hh', - 'build-result.hh', - 'build/derivation-goal.hh', - 'build/drv-output-substitution-goal.hh', - 'build/goal.hh', - 'build/substitution-goal.hh', - 'build/worker.hh', - 'builtins.hh', - 'builtins/buildenv.hh', - 'common-protocol-impl.hh', - 'common-protocol.hh', - 'common-ssh-store-config.hh', - 'content-address.hh', - 'daemon.hh', - 'derivations.hh', - 'derived-path-map.hh', - 'derived-path.hh', - 'downstream-placeholder.hh', - 'filetransfer.hh', - 'gc-store.hh', - 'globals.hh', - 'http-binary-cache-store.hh', - 'indirect-root-store.hh', - 'keys.hh', - 'legacy-ssh-store.hh', - 'length-prefixed-protocol-helper.hh', - 'local-binary-cache-store.hh', - 'local-fs-store.hh', - 'local-overlay-store.hh', - 'local-store.hh', - 'log-store.hh', - 'machines.hh', - 'make-content-addressed.hh', - 'names.hh', - 'nar-accessor.hh', - 'nar-info-disk-cache.hh', - 'nar-info.hh', - 'outputs-spec.hh', - 'parsed-derivations.hh', - 'path-info.hh', - 'path-references.hh', - 'path-regex.hh', - 'path-with-outputs.hh', - 'path.hh', - 'pathlocks.hh', - 'posix-fs-canonicalise.hh', - 'profiles.hh', - 'realisation.hh', - 'remote-fs-accessor.hh', - 'remote-store-connection.hh', - 'remote-store.hh', - 's3-binary-cache-store.hh', - 's3.hh', - 'ssh-store.hh', - 'serve-protocol-connection.hh', - 'serve-protocol-impl.hh', - 'serve-protocol.hh', - 'sqlite.hh', - 'ssh.hh', - 'store-api.hh', - 'store-cast.hh', - 'store-dir-config.hh', - 'store-reference.hh', - 'uds-remote-store.hh', - 'worker-protocol-connection.hh', - 'worker-protocol-impl.hh', - 'worker-protocol.hh', -) +subdir('include/nix/store') if host_machine.system() == 'linux' subdir('linux') @@ -333,105 +335,22 @@ else subdir('unix') endif -fs = import('fs') - -prefix = get_option('prefix') -# For each of these paths, assume that it is relative to the prefix unless -# it is already an absolute path (which is the default for store-dir, localstatedir, and log-dir). -path_opts = [ - # Meson built-ins. - 'datadir', - 'mandir', - 'libdir', - 'includedir', - 'libexecdir', - # Homecooked Nix directories. - 'store-dir', - 'localstatedir', - 'log-dir', -] -# For your grepping pleasure, this loop sets the following variables that aren't mentioned -# literally above: -# store_dir -# localstatedir -# log_dir -# profile_dir -foreach optname : path_opts - varname = optname.replace('-', '_') - path = get_option(optname) - if fs.is_absolute(path) - set_variable(varname, path) - else - set_variable(varname, prefix / path) - endif -endforeach - -# sysconfdir doesn't get anything installed to directly, and is only used to -# tell Nix where to look for nix.conf, so it doesn't get appended to prefix. -sysconfdir = get_option('sysconfdir') -if not fs.is_absolute(sysconfdir) - sysconfdir = '/' / sysconfdir -endif - -lsof = find_program('lsof', required : false) - -# Aside from prefix itself, each of these was made into an absolute path -# by joining it with prefix, unless it was already an absolute path -# (which is the default for store-dir, localstatedir, and log-dir). -cpp_str_defines = { - 'NIX_PREFIX': prefix, - 'NIX_STORE_DIR': store_dir, - 'NIX_DATA_DIR': datadir, - 'NIX_STATE_DIR': localstatedir / 'nix', - 'NIX_LOG_DIR': log_dir, - 'NIX_CONF_DIR': sysconfdir / 'nix', - 'NIX_MAN_DIR': mandir, -} - -if lsof.found() - lsof_path = lsof.full_path() -else - # Just look up on the PATH - lsof_path = 'lsof' -endif -cpp_str_defines += { - 'LSOF': lsof_path -} - -if get_option('embedded-sandbox-shell') - cpp_str_defines += { - 'SANDBOX_SHELL': '__embedded_sandbox_shell__' - } -elif busybox.found() - cpp_str_defines += { - 'SANDBOX_SHELL': busybox.full_path() - } -endif - -cpp_args = [] - -foreach name, value : cpp_str_defines - cpp_args += [ - '-D' + name + '=' + '"' + value + '"' - ] -endforeach - -subdir('build-utils-meson/export-all-symbols') -subdir('build-utils-meson/windows-version') +subdir('nix-meson-build-support/export-all-symbols') +subdir('nix-meson-build-support/windows-version') this_library = library( 'nixstore', generated_headers, sources, + config_priv_h, dependencies : deps_public + deps_private + deps_other, include_directories : include_dirs, - cpp_args : cpp_args, link_args: linker_export_flags, prelink : true, # For C++ static initializers install : true, ) -install_headers(headers, subdir : 'nix', preserve_path : true) +install_headers(headers, subdir : 'nix/store', preserve_path : true) libraries_private = [] @@ -446,4 +365,4 @@ if host_machine.system() != 'darwin' } endif -subdir('build-utils-meson/export') +subdir('nix-meson-build-support/export') diff --git a/src/libstore/misc.cc b/src/libstore/misc.cc index bcc02206bc9..7c97dbc5717 100644 --- a/src/libstore/misc.cc +++ b/src/libstore/misc.cc @@ -1,16 +1,17 @@ #include -#include "derivations.hh" -#include "parsed-derivations.hh" -#include "globals.hh" -#include "store-api.hh" -#include "thread-pool.hh" -#include "realisation.hh" -#include "topo-sort.hh" -#include "callback.hh" -#include "closure.hh" -#include "filetransfer.hh" -#include "strings.hh" +#include "nix/store/derivations.hh" +#include "nix/store/parsed-derivations.hh" +#include "nix/store/derivation-options.hh" +#include "nix/store/globals.hh" +#include "nix/store/store-open.hh" +#include "nix/util/thread-pool.hh" +#include "nix/store/realisation.hh" +#include "nix/util/topo-sort.hh" +#include "nix/util/callback.hh" +#include "nix/util/closure.hh" +#include "nix/store/filetransfer.hh" +#include "nix/util/strings.hh" namespace nix { @@ -97,23 +98,17 @@ const ContentAddress * getDerivationCA(const BasicDerivation & drv) return nullptr; } -void Store::queryMissing(const std::vector & targets, - StorePathSet & willBuild_, StorePathSet & willSubstitute_, StorePathSet & unknown_, - uint64_t & downloadSize_, uint64_t & narSize_) +MissingPaths Store::queryMissing(const std::vector & targets) { Activity act(*logger, lvlDebug, actUnknown, "querying info about missing paths"); - downloadSize_ = narSize_ = 0; - // FIXME: make async. ThreadPool pool(fileTransferSettings.httpConnections); struct State { std::unordered_set done; - StorePathSet & unknown, & willSubstitute, & willBuild; - uint64_t & downloadSize; - uint64_t & narSize; + MissingPaths res; }; struct DrvState @@ -124,7 +119,7 @@ void Store::queryMissing(const std::vector & targets, DrvState(size_t left) : left(left) { } }; - Sync state_(State{{}, unknown_, willSubstitute_, willBuild_, downloadSize_, narSize_}); + Sync state_; std::function doPath; @@ -142,7 +137,7 @@ void Store::queryMissing(const std::vector & targets, auto mustBuildDrv = [&](const StorePath & drvPath, const Derivation & drv) { { auto state(state_.lock()); - state->willBuild.insert(drvPath); + state->res.willBuild.insert(drvPath); } for (const auto & [inputDrv, inputNode] : drv.inputDrvs.map) { @@ -202,7 +197,7 @@ void Store::queryMissing(const std::vector & targets, if (!isValidPath(drvPath)) { // FIXME: we could try to substitute the derivation. auto state(state_.lock()); - state->unknown.insert(drvPath); + state->res.unknown.insert(drvPath); return; } @@ -221,9 +216,20 @@ void Store::queryMissing(const std::vector & targets, if (knownOutputPaths && invalid.empty()) return; auto drv = make_ref(derivationFromPath(drvPath)); - ParsedDerivation parsedDrv(StorePath(drvPath), *drv); + auto parsedDrv = StructuredAttrs::tryParse(drv->env); + DerivationOptions drvOptions; + try { + // FIXME: this is a lot of work just to get the value + // of `allowSubstitutes`. + drvOptions = DerivationOptions::fromStructuredAttrs( + drv->env, + parsedDrv ? &*parsedDrv : nullptr); + } catch (Error & e) { + e.addTrace({}, "while parsing derivation '%s'", printStorePath(drvPath)); + throw; + } - if (!knownOutputPaths && settings.useSubstitutes && parsedDrv.substitutesAllowed()) { + if (!knownOutputPaths && settings.useSubstitutes && drvOptions.substitutesAllowed()) { experimentalFeatureSettings.require(Xp::CaDerivations); // If there are unknown output paths, attempt to find if the @@ -253,7 +259,7 @@ void Store::queryMissing(const std::vector & targets, } } - if (knownOutputPaths && settings.useSubstitutes && parsedDrv.substitutesAllowed()) { + if (knownOutputPaths && settings.useSubstitutes && drvOptions.substitutesAllowed()) { auto drvState = make_ref>(DrvState(invalid.size())); for (auto & output : invalid) pool.enqueue(std::bind(checkOutput, drvPath, drv, output, drvState)); @@ -270,7 +276,7 @@ void Store::queryMissing(const std::vector & targets, if (infos.empty()) { auto state(state_.lock()); - state->unknown.insert(bo.path); + state->res.unknown.insert(bo.path); return; } @@ -279,9 +285,9 @@ void Store::queryMissing(const std::vector & targets, { auto state(state_.lock()); - state->willSubstitute.insert(bo.path); - state->downloadSize += info->second.downloadSize; - state->narSize += info->second.narSize; + state->res.willSubstitute.insert(bo.path); + state->res.downloadSize += info->second.downloadSize; + state->res.narSize += info->second.narSize; } for (auto & ref : info->second.references) @@ -294,6 +300,8 @@ void Store::queryMissing(const std::vector & targets, pool.enqueue(std::bind(doPath, path)); pool.process(); + + return std::move(state_.lock()->res); } diff --git a/src/libstore/names.cc b/src/libstore/names.cc index c0e1b1022ac..998b9356a2a 100644 --- a/src/libstore/names.cc +++ b/src/libstore/names.cc @@ -1,5 +1,5 @@ -#include "names.hh" -#include "util.hh" +#include "nix/store/names.hh" +#include "nix/util/util.hh" #include diff --git a/src/libstore/nar-accessor.cc b/src/libstore/nar-accessor.cc index 9a541bb7776..6aba68a368b 100644 --- a/src/libstore/nar-accessor.cc +++ b/src/libstore/nar-accessor.cc @@ -1,5 +1,5 @@ -#include "nar-accessor.hh" -#include "archive.hh" +#include "nix/store/nar-accessor.hh" +#include "nix/util/archive.hh" #include #include @@ -291,7 +291,11 @@ json listNar(ref accessor, const CanonPath & path, bool recurse) obj["type"] = "symlink"; obj["target"] = accessor->readLink(path); break; - case SourceAccessor::Type::tMisc: + case SourceAccessor::Type::tBlock: + case SourceAccessor::Type::tChar: + case SourceAccessor::Type::tSocket: + case SourceAccessor::Type::tFifo: + case SourceAccessor::Type::tUnknown: assert(false); // cannot happen for NARs } return obj; diff --git a/src/libstore/nar-info-disk-cache.cc b/src/libstore/nar-info-disk-cache.cc index 80e8d34149d..5d72ba8aea2 100644 --- a/src/libstore/nar-info-disk-cache.cc +++ b/src/libstore/nar-info-disk-cache.cc @@ -1,13 +1,13 @@ -#include "nar-info-disk-cache.hh" -#include "users.hh" -#include "sync.hh" -#include "sqlite.hh" -#include "globals.hh" +#include "nix/store/nar-info-disk-cache.hh" +#include "nix/util/users.hh" +#include "nix/util/sync.hh" +#include "nix/store/sqlite.hh" +#include "nix/store/globals.hh" #include #include -#include "strings.hh" +#include "nix/util/strings.hh" namespace nix { diff --git a/src/libstore/nar-info.cc b/src/libstore/nar-info.cc index 27fcc286411..ef7af6126e2 100644 --- a/src/libstore/nar-info.cc +++ b/src/libstore/nar-info.cc @@ -1,8 +1,8 @@ -#include "globals.hh" -#include "nar-info.hh" -#include "store-api.hh" -#include "strings.hh" -#include "json-utils.hh" +#include "nix/store/globals.hh" +#include "nix/store/nar-info.hh" +#include "nix/store/store-api.hh" +#include "nix/util/strings.hh" +#include "nix/util/json-utils.hh" namespace nix { @@ -176,7 +176,7 @@ NarInfo NarInfo::fromJSON( std::nullopt); if (json.contains("downloadSize")) - res.fileSize = getInteger(valueAt(json, "downloadSize")); + res.fileSize = getUnsigned(valueAt(json, "downloadSize")); return res; } diff --git a/src/libstore/nix-meson-build-support b/src/libstore/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/libstore/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/libstore/optimise-store.cc b/src/libstore/optimise-store.cc index aeff24c642a..e47c0707c02 100644 --- a/src/libstore/optimise-store.cc +++ b/src/libstore/optimise-store.cc @@ -1,8 +1,8 @@ -#include "local-store.hh" -#include "globals.hh" -#include "signals.hh" -#include "posix-fs-canonicalise.hh" -#include "posix-source-accessor.hh" +#include "nix/store/local-store.hh" +#include "nix/store/globals.hh" +#include "nix/util/signals.hh" +#include "nix/store/posix-fs-canonicalise.hh" +#include "nix/util/posix-source-accessor.hh" #include #include @@ -13,6 +13,7 @@ #include #include +#include "store-config-private.hh" namespace nix { @@ -96,11 +97,11 @@ void LocalStore::optimisePath_(Activity * act, OptimiseStats & stats, auto st = lstat(path); -#if __APPLE__ +#ifdef __APPLE__ /* HFS/macOS has some undocumented security feature disabling hardlinking for special files within .app dirs. Known affected paths include *.app/Contents/{PkgInfo,Resources/\*.lproj,_CodeSignature} and .DS_Store. - See https://github.com/NixOS/nix/issues/1443 and + See https://github.com/NixOS/nix/issues/1443 and https://github.com/NixOS/nix/pull/2230 for more discussion. */ if (std::regex_search(path, std::regex("\\.app/Contents/.+$"))) @@ -215,14 +216,14 @@ void LocalStore::optimisePath_(Activity * act, OptimiseStats & stats, the store itself (we don't want or need to mess with its permissions). */ const Path dirOfPath(dirOf(path)); - bool mustToggle = dirOfPath != realStoreDir.get(); + bool mustToggle = dirOfPath != config->realStoreDir.get(); if (mustToggle) makeWritable(dirOfPath); /* When we're done, make the directory read-only again and reset its timestamp back to 0. */ MakeReadOnly makeReadOnly(mustToggle ? dirOfPath : ""); - std::filesystem::path tempLink = fmt("%1%/.tmp-link-%2%-%3%", realStoreDir, getpid(), rand()); + std::filesystem::path tempLink = fmt("%1%/.tmp-link-%2%-%3%", config->realStoreDir, getpid(), rand()); try { std::filesystem::create_hard_link(linkPath, tempLink); @@ -284,7 +285,7 @@ void LocalStore::optimiseStore(OptimiseStats & stats) if (!isValidPath(i)) continue; /* path was GC'ed, probably */ { Activity act(*logger, lvlTalkative, actUnknown, fmt("optimising path '%s'", printStorePath(i))); - optimisePath_(&act, stats, realStoreDir + "/" + std::string(i.to_string()), inodeHash, NoRepair); + optimisePath_(&act, stats, config->realStoreDir + "/" + std::string(i.to_string()), inodeHash, NoRepair); } done++; act.progress(done, paths.size()); diff --git a/src/libstore/outputs-spec.cc b/src/libstore/outputs-spec.cc index b623a975cc4..28fe45de91e 100644 --- a/src/libstore/outputs-spec.cc +++ b/src/libstore/outputs-spec.cc @@ -1,11 +1,11 @@ #include #include -#include "util.hh" -#include "regex-combinators.hh" -#include "outputs-spec.hh" -#include "path-regex.hh" -#include "strings-inline.hh" +#include "nix/util/util.hh" +#include "nix/util/regex-combinators.hh" +#include "nix/store/outputs-spec.hh" +#include "nix/store/path-regex.hh" +#include "nix/util/strings-inline.hh" namespace nix { diff --git a/src/libstore/package.nix b/src/libstore/package.nix index f04e3b95fc3..775776139ae 100644 --- a/src/libstore/package.nix +++ b/src/libstore/package.nix @@ -1,25 +1,30 @@ -{ lib -, stdenv -, mkMesonLibrary +{ + lib, + stdenv, + mkMesonLibrary, -, unixtools -, darwin + unixtools, + darwin, -, nix-util -, boost -, curl -, aws-sdk-cpp -, libseccomp -, nlohmann_json -, sqlite + nix-util, + boost, + curl, + aws-sdk-cpp, + libseccomp, + nlohmann_json, + sqlite, -, busybox-sandbox-shell ? null + busybox-sandbox-shell ? null, -# Configuration Options + # Configuration Options -, version + version, -, embeddedSandboxShell ? stdenv.hostPlatform.isStatic + embeddedSandboxShell ? stdenv.hostPlatform.isStatic, + + withAWS ? + # Default is this way because there have been issues building this dependency + stdenv.hostPlatform == stdenv.buildPlatform && (stdenv.isLinux || stdenv.isDarwin), }: let @@ -32,14 +37,17 @@ mkMesonLibrary (finalAttrs: { workDir = ./.; fileset = fileset.unions [ - ../../build-utils-meson - ./build-utils-meson + ../../nix-meson-build-support + ./nix-meson-build-support ../../.version ./.version ./meson.build ./meson.options + ./include/nix/store/meson.build ./linux/meson.build + ./linux/include/nix/store/meson.build ./unix/meson.build + ./unix/include/nix/store/meson.build ./windows/meson.build (fileset.fileFilter (file: file.hasExt "cc") ./.) (fileset.fileFilter (file: file.hasExt "hh") ./.) @@ -48,48 +56,32 @@ mkMesonLibrary (finalAttrs: { (fileset.fileFilter (file: file.hasExt "sql") ./.) ]; - nativeBuildInputs = - lib.optional embeddedSandboxShell unixtools.hexdump; + nativeBuildInputs = lib.optional embeddedSandboxShell unixtools.hexdump; - buildInputs = [ - boost - curl - sqlite - ] ++ lib.optional stdenv.hostPlatform.isLinux libseccomp + buildInputs = + [ + boost + curl + sqlite + ] + ++ lib.optional stdenv.hostPlatform.isLinux libseccomp # There have been issues building these dependencies ++ lib.optional stdenv.hostPlatform.isDarwin darwin.apple_sdk.libs.sandbox - ++ lib.optional (stdenv.hostPlatform == stdenv.buildPlatform && (stdenv.isLinux || stdenv.isDarwin)) - aws-sdk-cpp - ; + ++ lib.optional withAWS aws-sdk-cpp; propagatedBuildInputs = [ nix-util nlohmann_json ]; - preConfigure = - # "Inline" .version so it's not a symlink, and includes the suffix. - # Do the meson utils, without modification. - '' - chmod u+w ./.version - echo ${version} > ../../.version - ''; - - mesonFlags = [ - (lib.mesonEnable "seccomp-sandboxing" stdenv.hostPlatform.isLinux) - (lib.mesonBool "embedded-sandbox-shell" embeddedSandboxShell) - ] ++ lib.optionals stdenv.hostPlatform.isLinux [ - (lib.mesonOption "sandbox-shell" "${busybox-sandbox-shell}/bin/busybox") - ]; - - env = { - # Needed for Meson to find Boost. - # https://github.com/NixOS/nixpkgs/issues/86131. - BOOST_INCLUDEDIR = "${lib.getDev boost}/include"; - BOOST_LIBRARYDIR = "${lib.getLib boost}/lib"; - } // lib.optionalAttrs (stdenv.isLinux && !(stdenv.hostPlatform.isStatic && stdenv.system == "aarch64-linux")) { - LDFLAGS = "-fuse-ld=gold"; - }; + mesonFlags = + [ + (lib.mesonEnable "seccomp-sandboxing" stdenv.hostPlatform.isLinux) + (lib.mesonBool "embedded-sandbox-shell" embeddedSandboxShell) + ] + ++ lib.optionals stdenv.hostPlatform.isLinux [ + (lib.mesonOption "sandbox-shell" "${busybox-sandbox-shell}/bin/busybox") + ]; meta = { platforms = lib.platforms.unix ++ lib.platforms.windows; diff --git a/src/libstore/parsed-derivations.cc b/src/libstore/parsed-derivations.cc index d8459d4d71c..d6453c6db6a 100644 --- a/src/libstore/parsed-derivations.cc +++ b/src/libstore/parsed-derivations.cc @@ -1,133 +1,27 @@ -#include "parsed-derivations.hh" +#include "nix/store/parsed-derivations.hh" +#include "nix/store/store-api.hh" +#include "nix/store/derivations.hh" +#include "nix/store/derivation-options.hh" #include #include namespace nix { -ParsedDerivation::ParsedDerivation(const StorePath & drvPath, BasicDerivation & drv) - : drvPath(drvPath), drv(drv) +std::optional StructuredAttrs::tryParse(const StringPairs & env) { /* Parse the __json attribute, if any. */ - auto jsonAttr = drv.env.find("__json"); - if (jsonAttr != drv.env.end()) { + auto jsonAttr = env.find("__json"); + if (jsonAttr != env.end()) { try { - structuredAttrs = std::make_unique(nlohmann::json::parse(jsonAttr->second)); + return StructuredAttrs { + .structuredAttrs = nlohmann::json::parse(jsonAttr->second), + }; } catch (std::exception & e) { - throw Error("cannot process __json attribute of '%s': %s", drvPath.to_string(), e.what()); + throw Error("cannot process __json attribute: %s", e.what()); } } -} - -ParsedDerivation::~ParsedDerivation() { } - -std::optional ParsedDerivation::getStringAttr(const std::string & name) const -{ - if (structuredAttrs) { - auto i = structuredAttrs->find(name); - if (i == structuredAttrs->end()) - return {}; - else { - if (!i->is_string()) - throw Error("attribute '%s' of derivation '%s' must be a string", name, drvPath.to_string()); - return i->get(); - } - } else { - auto i = drv.env.find(name); - if (i == drv.env.end()) - return {}; - else - return i->second; - } -} - -bool ParsedDerivation::getBoolAttr(const std::string & name, bool def) const -{ - if (structuredAttrs) { - auto i = structuredAttrs->find(name); - if (i == structuredAttrs->end()) - return def; - else { - if (!i->is_boolean()) - throw Error("attribute '%s' of derivation '%s' must be a Boolean", name, drvPath.to_string()); - return i->get(); - } - } else { - auto i = drv.env.find(name); - if (i == drv.env.end()) - return def; - else - return i->second == "1"; - } -} - -std::optional ParsedDerivation::getStringsAttr(const std::string & name) const -{ - if (structuredAttrs) { - auto i = structuredAttrs->find(name); - if (i == structuredAttrs->end()) - return {}; - else { - if (!i->is_array()) - throw Error("attribute '%s' of derivation '%s' must be a list of strings", name, drvPath.to_string()); - Strings res; - for (auto j = i->begin(); j != i->end(); ++j) { - if (!j->is_string()) - throw Error("attribute '%s' of derivation '%s' must be a list of strings", name, drvPath.to_string()); - res.push_back(j->get()); - } - return res; - } - } else { - auto i = drv.env.find(name); - if (i == drv.env.end()) - return {}; - else - return tokenizeString(i->second); - } -} - -StringSet ParsedDerivation::getRequiredSystemFeatures() const -{ - // FIXME: cache this? - StringSet res; - for (auto & i : getStringsAttr("requiredSystemFeatures").value_or(Strings())) - res.insert(i); - if (!drv.type().hasKnownOutputPaths()) - res.insert("ca-derivations"); - return res; -} - -bool ParsedDerivation::canBuildLocally(Store & localStore) const -{ - if (drv.platform != settings.thisSystem.get() - && !settings.extraPlatforms.get().count(drv.platform) - && !drv.isBuiltin()) - return false; - - if (settings.maxBuildJobs.get() == 0 - && !drv.isBuiltin()) - return false; - - for (auto & feature : getRequiredSystemFeatures()) - if (!localStore.systemFeatures.get().count(feature)) return false; - - return true; -} - -bool ParsedDerivation::willBuildLocally(Store & localStore) const -{ - return getBoolAttr("preferLocalBuild") && canBuildLocally(localStore); -} - -bool ParsedDerivation::substitutesAllowed() const -{ - return settings.alwaysAllowSubstitutes ? true : getBoolAttr("allowSubstitutes", true); -} - -bool ParsedDerivation::useUidRange() const -{ - return getRequiredSystemFeatures().count("uid-range"); + return {}; } static std::regex shVarName("[A-Za-z_][A-Za-z0-9_]*"); @@ -186,45 +80,39 @@ static nlohmann::json pathInfoToJSON( return jsonList; } -std::optional ParsedDerivation::prepareStructuredAttrs(Store & store, const StorePathSet & inputPaths) +nlohmann::json StructuredAttrs::prepareStructuredAttrs( + Store & store, + const DerivationOptions & drvOptions, + const StorePathSet & inputPaths, + const DerivationOutputs & outputs) const { - auto structuredAttrs = getStructuredAttrs(); - if (!structuredAttrs) return std::nullopt; - - auto json = *structuredAttrs; + /* Copy to then modify */ + auto json = structuredAttrs; /* Add an "outputs" object containing the output paths. */ - nlohmann::json outputs; - for (auto & i : drv.outputs) - outputs[i.first] = hashPlaceholder(i.first); - json["outputs"] = outputs; + nlohmann::json outputsJson; + for (auto & i : outputs) + outputsJson[i.first] = hashPlaceholder(i.first); + json["outputs"] = std::move(outputsJson); /* Handle exportReferencesGraph. */ - auto e = json.find("exportReferencesGraph"); - if (e != json.end() && e->is_object()) { - for (auto i = e->begin(); i != e->end(); ++i) { - StorePathSet storePaths; - for (auto & p : *i) - storePaths.insert(store.toStorePath(p.get()).first); - json[i.key()] = pathInfoToJSON(store, - store.exportReferences(storePaths, inputPaths)); - } + for (auto & [key, inputPaths] : drvOptions.exportReferencesGraph) { + StorePathSet storePaths; + for (auto & p : inputPaths) + storePaths.insert(store.toStorePath(p).first); + json[key] = pathInfoToJSON(store, + store.exportReferences(storePaths, storePaths)); } return json; } -/* As a convenience to bash scripts, write a shell file that - maps all attributes that are representable in bash - - namely, strings, integers, nulls, Booleans, and arrays and - objects consisting entirely of those values. (So nested - arrays or objects are not supported.) */ -std::string writeStructuredAttrsShell(const nlohmann::json & json) +std::string StructuredAttrs::writeShell(const nlohmann::json & json) { auto handleSimpleType = [](const nlohmann::json & value) -> std::optional { if (value.is_string()) - return shellEscape(value.get()); + return escapeShellArgAlways(value.get()); if (value.is_number()) { auto f = value.get(); @@ -272,7 +160,7 @@ std::string writeStructuredAttrsShell(const nlohmann::json & json) for (auto & [key2, value2] : value.items()) { auto s3 = handleSimpleType(value2); if (!s3) { good = false; break; } - s2 += fmt("[%s]=%s ", shellEscape(key2), *s3); + s2 += fmt("[%s]=%s ", escapeShellArgAlways(key2), *s3); } if (good) diff --git a/src/libstore/parsed-derivations.hh b/src/libstore/parsed-derivations.hh deleted file mode 100644 index 71085a604d4..00000000000 --- a/src/libstore/parsed-derivations.hh +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once -///@file - -#include "derivations.hh" -#include "store-api.hh" - -#include - -namespace nix { - -class ParsedDerivation -{ - StorePath drvPath; - BasicDerivation & drv; - std::unique_ptr structuredAttrs; - -public: - - ParsedDerivation(const StorePath & drvPath, BasicDerivation & drv); - - ~ParsedDerivation(); - - const nlohmann::json * getStructuredAttrs() const - { - return structuredAttrs.get(); - } - - std::optional getStringAttr(const std::string & name) const; - - bool getBoolAttr(const std::string & name, bool def = false) const; - - std::optional getStringsAttr(const std::string & name) const; - - StringSet getRequiredSystemFeatures() const; - - bool canBuildLocally(Store & localStore) const; - - bool willBuildLocally(Store & localStore) const; - - bool substitutesAllowed() const; - - bool useUidRange() const; - - std::optional prepareStructuredAttrs(Store & store, const StorePathSet & inputPaths); -}; - -std::string writeStructuredAttrsShell(const nlohmann::json & json); - -} diff --git a/src/libstore/path-info.cc b/src/libstore/path-info.cc index 6e87e60f446..17514643557 100644 --- a/src/libstore/path-info.cc +++ b/src/libstore/path-info.cc @@ -1,10 +1,10 @@ #include -#include "path-info.hh" -#include "store-api.hh" -#include "json-utils.hh" -#include "comparator.hh" -#include "strings.hh" +#include "nix/store/path-info.hh" +#include "nix/store/store-api.hh" +#include "nix/util/json-utils.hh" +#include "nix/util/comparator.hh" +#include "nix/util/strings.hh" namespace nix { @@ -40,6 +40,14 @@ void ValidPathInfo::sign(const Store & store, const Signer & signer) sigs.insert(signer.signDetached(fingerprint(store))); } +void ValidPathInfo::sign(const Store & store, const std::vector> & signers) +{ + auto fingerprint = this->fingerprint(store); + for (auto & signer: signers) { + sigs.insert(signer->signDetached(fingerprint)); + } +} + std::optional ValidPathInfo::contentAddressWithReferences() const { if (! ca) @@ -192,7 +200,7 @@ UnkeyedValidPathInfo UnkeyedValidPathInfo::fromJSON( auto & json = getObject(_json); res.narHash = Hash::parseAny(getString(valueAt(json, "narHash")), std::nullopt); - res.narSize = getInteger(valueAt(json, "narSize")); + res.narSize = getUnsigned(valueAt(json, "narSize")); try { auto references = getStringList(valueAt(json, "references")); @@ -216,7 +224,7 @@ UnkeyedValidPathInfo UnkeyedValidPathInfo::fromJSON( if (json.contains("registrationTime")) if (auto * rawRegistrationTime = getNullable(valueAt(json, "registrationTime"))) - res.registrationTime = getInteger(*rawRegistrationTime); + res.registrationTime = getInteger(*rawRegistrationTime); if (json.contains("ultimate")) res.ultimate = getBoolean(valueAt(json, "ultimate")); diff --git a/src/libstore/path-references.cc b/src/libstore/path-references.cc index 15f52ec9dea..c06647eb1e3 100644 --- a/src/libstore/path-references.cc +++ b/src/libstore/path-references.cc @@ -1,6 +1,6 @@ -#include "path-references.hh" -#include "hash.hh" -#include "archive.hh" +#include "nix/store/path-references.hh" +#include "nix/util/hash.hh" +#include "nix/util/archive.hh" #include #include diff --git a/src/libstore/path-with-outputs.cc b/src/libstore/path-with-outputs.cc index e526b1ff6c7..f3fc534ef3c 100644 --- a/src/libstore/path-with-outputs.cc +++ b/src/libstore/path-with-outputs.cc @@ -1,8 +1,8 @@ #include -#include "path-with-outputs.hh" -#include "store-api.hh" -#include "strings.hh" +#include "nix/store/path-with-outputs.hh" +#include "nix/store/store-api.hh" +#include "nix/util/strings.hh" namespace nix { @@ -82,9 +82,9 @@ std::pair parsePathWithOutputs(std::string_view s) { size_t n = s.find("!"); return n == s.npos - ? std::make_pair(s, std::set()) + ? std::make_pair(s, StringSet()) : std::make_pair(s.substr(0, n), - tokenizeString>(s.substr(n + 1), ",")); + tokenizeString(s.substr(n + 1), ",")); } diff --git a/src/libstore/path.cc b/src/libstore/path.cc index 3e9d054778c..d989b1caa0b 100644 --- a/src/libstore/path.cc +++ b/src/libstore/path.cc @@ -1,4 +1,4 @@ -#include "store-dir-config.hh" +#include "nix/store/store-dir-config.hh" namespace nix { @@ -75,7 +75,7 @@ StorePath StorePath::random(std::string_view name) return StorePath(Hash::random(HashAlgorithm::SHA1), name); } -StorePath StoreDirConfig::parseStorePath(std::string_view path) const +StorePath MixStoreDirMethods::parseStorePath(std::string_view path) const { // On Windows, `/nix/store` is not a canonical path. More broadly it // is unclear whether this function should be using the native @@ -94,7 +94,7 @@ StorePath StoreDirConfig::parseStorePath(std::string_view path) const return StorePath(baseNameOf(p)); } -std::optional StoreDirConfig::maybeParseStorePath(std::string_view path) const +std::optional MixStoreDirMethods::maybeParseStorePath(std::string_view path) const { try { return parseStorePath(path); @@ -103,24 +103,24 @@ std::optional StoreDirConfig::maybeParseStorePath(std::string_view pa } } -bool StoreDirConfig::isStorePath(std::string_view path) const +bool MixStoreDirMethods::isStorePath(std::string_view path) const { return (bool) maybeParseStorePath(path); } -StorePathSet StoreDirConfig::parseStorePathSet(const PathSet & paths) const +StorePathSet MixStoreDirMethods::parseStorePathSet(const PathSet & paths) const { StorePathSet res; for (auto & i : paths) res.insert(parseStorePath(i)); return res; } -std::string StoreDirConfig::printStorePath(const StorePath & path) const +std::string MixStoreDirMethods::printStorePath(const StorePath & path) const { return (storeDir + "/").append(path.to_string()); } -PathSet StoreDirConfig::printStorePathSet(const StorePathSet & paths) const +PathSet MixStoreDirMethods::printStorePathSet(const StorePathSet & paths) const { PathSet res; for (auto & i : paths) res.insert(printStorePath(i)); diff --git a/src/libstore/pathlocks.cc b/src/libstore/pathlocks.cc index c855e797fdc..34acfb02d19 100644 --- a/src/libstore/pathlocks.cc +++ b/src/libstore/pathlocks.cc @@ -1,7 +1,7 @@ -#include "pathlocks.hh" -#include "util.hh" -#include "sync.hh" -#include "signals.hh" +#include "nix/store/pathlocks.hh" +#include "nix/util/util.hh" +#include "nix/util/sync.hh" +#include "nix/util/signals.hh" #include #include diff --git a/src/libstore/posix-fs-canonicalise.cc b/src/libstore/posix-fs-canonicalise.cc index 46a78cc86aa..792fe5c76d1 100644 --- a/src/libstore/posix-fs-canonicalise.cc +++ b/src/libstore/posix-fs-canonicalise.cc @@ -1,14 +1,16 @@ -#if HAVE_ACL_SUPPORT +#include "nix/store/posix-fs-canonicalise.hh" +#include "nix/util/file-system.hh" +#include "nix/util/signals.hh" +#include "nix/util/util.hh" +#include "nix/store/globals.hh" +#include "nix/store/store-api.hh" + +#include "store-config-private.hh" + +#if NIX_SUPPORT_ACL # include #endif -#include "posix-fs-canonicalise.hh" -#include "file-system.hh" -#include "signals.hh" -#include "util.hh" -#include "globals.hh" -#include "store-api.hh" - namespace nix { const time_t mtimeStore = 1; /* 1 second into the epoch */ @@ -56,7 +58,7 @@ static void canonicalisePathMetaData_( { checkInterrupt(); -#if __APPLE__ +#ifdef __APPLE__ /* Remove flags, in particular UF_IMMUTABLE which would prevent the file from being garbage-collected. FIXME: Use setattrlist() to remove other attributes as well. */ @@ -72,7 +74,7 @@ static void canonicalisePathMetaData_( if (!(S_ISREG(st.st_mode) || S_ISDIR(st.st_mode) || S_ISLNK(st.st_mode))) throw Error("file '%1%' has an unsupported type", path); -#if HAVE_ACL_SUPPORT +#if NIX_SUPPORT_ACL /* Remove extended attributes / ACLs. */ ssize_t eaSize = llistxattr(path.c_str(), nullptr, 0); @@ -134,7 +136,7 @@ static void canonicalisePathMetaData_( #endif if (S_ISDIR(st.st_mode)) { - for (auto & i : std::filesystem::directory_iterator{path}) { + for (auto & i : DirectoryIterator{path}) { checkInterrupt(); canonicalisePathMetaData_( i.path().string(), diff --git a/src/libstore/profiles.cc b/src/libstore/profiles.cc index 46efedfe327..09ef36705fa 100644 --- a/src/libstore/profiles.cc +++ b/src/libstore/profiles.cc @@ -1,8 +1,8 @@ -#include "profiles.hh" -#include "signals.hh" -#include "store-api.hh" -#include "local-fs-store.hh" -#include "users.hh" +#include "nix/store/profiles.hh" +#include "nix/util/signals.hh" +#include "nix/store/store-api.hh" +#include "nix/store/local-fs-store.hh" +#include "nix/util/users.hh" #include #include @@ -38,7 +38,7 @@ std::pair> findGenerations(Path pro std::filesystem::path profileDir = dirOf(profile); auto profileName = std::string(baseNameOf(profile)); - for (auto & i : std::filesystem::directory_iterator{profileDir}) { + for (auto & i : DirectoryIterator{profileDir}) { checkInterrupt(); if (auto n = parseName(profileName, i.path().filename().string())) { auto path = i.path().string(); @@ -331,7 +331,7 @@ Path getDefaultProfile() if (!pathExists(profileLink)) { replaceSymlink(profile, profileLink); } - // Backwards compatibiliy measure: Make root's profile available as + // Backwards compatibility measure: Make root's profile available as // `.../default` as it's what NixOS and most of the init scripts expect Path globalProfileLink = settings.nixStateDir + "/profiles/default"; if (isRootUser() && !pathExists(globalProfileLink)) { diff --git a/src/libstore/realisation.cc b/src/libstore/realisation.cc index 86bfdd1a8bf..9a72422eb89 100644 --- a/src/libstore/realisation.cc +++ b/src/libstore/realisation.cc @@ -1,7 +1,7 @@ -#include "realisation.hh" -#include "store-api.hh" -#include "closure.hh" -#include "signature/local-keys.hh" +#include "nix/store/realisation.hh" +#include "nix/store/store-api.hh" +#include "nix/util/closure.hh" +#include "nix/util/signature/local-keys.hh" #include namespace nix { @@ -96,7 +96,7 @@ Realisation Realisation::fromJSON( std::map dependentRealisations; if (auto jsonDependencies = json.find("dependentRealisations"); jsonDependencies != json.end()) - for (auto & [jsonDepId, jsonDepOutPath] : jsonDependencies->get>()) + for (auto & [jsonDepId, jsonDepOutPath] : jsonDependencies->get()) dependentRealisations.insert({DrvOutput::parse(jsonDepId), StorePath(jsonDepOutPath)}); return Realisation{ diff --git a/src/libstore/remote-fs-accessor.cc b/src/libstore/remote-fs-accessor.cc index 7e360b5fef1..fdbe12fa914 100644 --- a/src/libstore/remote-fs-accessor.cc +++ b/src/libstore/remote-fs-accessor.cc @@ -1,6 +1,6 @@ #include -#include "remote-fs-accessor.hh" -#include "nar-accessor.hh" +#include "nix/store/remote-fs-accessor.hh" +#include "nix/store/nar-accessor.hh" #include #include @@ -51,7 +51,7 @@ ref RemoteFSAccessor::addToCache(std::string_view hashPart, std: std::pair, CanonPath> RemoteFSAccessor::fetch(const CanonPath & path) { - auto [storePath, restPath_] = store->toStorePath(path.abs()); + auto [storePath, restPath_] = store->toStorePath(store->storeDir + path.abs()); auto restPath = CanonPath(restPath_); if (requireValidPath && !store->isValidPath(storePath)) diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index 69bbc64fca3..1b8bad04807 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -1,34 +1,34 @@ -#include "serialise.hh" -#include "util.hh" -#include "path-with-outputs.hh" -#include "gc-store.hh" -#include "remote-fs-accessor.hh" -#include "build-result.hh" -#include "remote-store.hh" -#include "remote-store-connection.hh" -#include "worker-protocol.hh" -#include "worker-protocol-impl.hh" -#include "archive.hh" -#include "globals.hh" -#include "derivations.hh" -#include "pool.hh" -#include "finally.hh" -#include "git.hh" -#include "logging.hh" -#include "callback.hh" -#include "filetransfer.hh" -#include "signals.hh" +#include "nix/util/serialise.hh" +#include "nix/util/util.hh" +#include "nix/store/path-with-outputs.hh" +#include "nix/store/gc-store.hh" +#include "nix/store/remote-fs-accessor.hh" +#include "nix/store/build-result.hh" +#include "nix/store/remote-store.hh" +#include "nix/store/remote-store-connection.hh" +#include "nix/store/worker-protocol.hh" +#include "nix/store/worker-protocol-impl.hh" +#include "nix/util/archive.hh" +#include "nix/store/globals.hh" +#include "nix/store/derivations.hh" +#include "nix/util/pool.hh" +#include "nix/util/finally.hh" +#include "nix/util/git.hh" +#include "nix/util/logging.hh" +#include "nix/util/callback.hh" +#include "nix/store/filetransfer.hh" +#include "nix/util/signals.hh" #include namespace nix { /* TODO: Separate these store types into different files, give them better names */ -RemoteStore::RemoteStore(const Params & params) - : RemoteStoreConfig(params) - , Store(params) +RemoteStore::RemoteStore(const Config & config) + : Store{config} + , config{config} , connections(make_ref>( - std::max(1, (int) maxConnections), + std::max(1, config.maxConnections.get()), [this]() { auto conn = openConnectionWrapper(); try { @@ -44,7 +44,7 @@ RemoteStore::RemoteStore(const Params & params) r->to.good() && r->from.good() && std::chrono::duration_cast( - std::chrono::steady_clock::now() - r->startTime).count() < maxConnectionAge; + std::chrono::steady_clock::now() - r->startTime).count() < this->config.maxConnectionAge; } )) { @@ -122,7 +122,7 @@ void RemoteStore::setOptions(Connection & conn) << settings.useSubstitutes; if (GET_PROTOCOL_MINOR(conn.protoVersion) >= 12) { - std::map overrides; + std::map overrides; settings.getSettings(overrides, true); // libstore settings fileTransferSettings.getSettings(overrides, true); overrides.erase(settings.keepFailed.name); @@ -534,14 +534,27 @@ void RemoteStore::addToStore(const ValidPathInfo & info, Source & source, void RemoteStore::addMultipleToStore( - PathsSource & pathsToCopy, + PathsSource && pathsToCopy, Activity & act, RepairFlag repair, CheckSigsFlag checkSigs) { + // `addMultipleToStore` is single threaded + size_t bytesExpected = 0; + for (auto & [pathInfo, _] : pathsToCopy) { + bytesExpected += pathInfo.narSize; + } + act.setExpected(actCopyPath, bytesExpected); + auto source = sinkToSource([&](Sink & sink) { - sink << pathsToCopy.size(); - for (auto & [pathInfo, pathSource] : pathsToCopy) { + size_t nrTotal = pathsToCopy.size(); + sink << nrTotal; + // Reverse, so we can release memory at the original start + std::reverse(pathsToCopy.begin(), pathsToCopy.end()); + while (!pathsToCopy.empty()) { + act.progress(nrTotal - pathsToCopy.size(), nrTotal, size_t(1), size_t(0)); + + auto & [pathInfo, pathSource] = pathsToCopy.back(); WorkerProto::Serialise::write(*this, WorkerProto::WriteConn { .to = sink, @@ -549,6 +562,7 @@ void RemoteStore::addMultipleToStore( }, pathInfo); pathSource->drainInto(sink); + pathsToCopy.pop_back(); } }); @@ -594,7 +608,7 @@ void RemoteStore::queryRealisationUncached(const DrvOutput & id, auto conn(getConnection()); if (GET_PROTOCOL_MINOR(conn->protoVersion) < 27) { - warn("the daemon is too old to support content-addressed derivations, please upgrade it to 2.4"); + warn("the daemon is too old to support content-addressing derivations, please upgrade it to 2.4"); return callback(nullptr); } @@ -841,9 +855,7 @@ void RemoteStore::addSignatures(const StorePath & storePath, const StringSet & s } -void RemoteStore::queryMissing(const std::vector & targets, - StorePathSet & willBuild, StorePathSet & willSubstitute, StorePathSet & unknown, - uint64_t & downloadSize, uint64_t & narSize) +MissingPaths RemoteStore::queryMissing(const std::vector & targets) { { auto conn(getConnection()); @@ -854,16 +866,16 @@ void RemoteStore::queryMissing(const std::vector & targets, conn->to << WorkerProto::Op::QueryMissing; WorkerProto::write(*this, *conn, targets); conn.processStderr(); - willBuild = WorkerProto::Serialise::read(*this, *conn); - willSubstitute = WorkerProto::Serialise::read(*this, *conn); - unknown = WorkerProto::Serialise::read(*this, *conn); - conn->from >> downloadSize >> narSize; - return; + MissingPaths res; + res.willBuild = WorkerProto::Serialise::read(*this, *conn); + res.willSubstitute = WorkerProto::Serialise::read(*this, *conn); + res.unknown = WorkerProto::Serialise::read(*this, *conn); + conn->from >> res.downloadSize >> res.narSize; + return res; } fallback: - return Store::queryMissing(targets, willBuild, willSubstitute, - unknown, downloadSize, narSize); + return Store::queryMissing(targets); } diff --git a/src/libstore/restricted-store.cc b/src/libstore/restricted-store.cc new file mode 100644 index 00000000000..69435122a24 --- /dev/null +++ b/src/libstore/restricted-store.cc @@ -0,0 +1,326 @@ +#include "nix/store/restricted-store.hh" +#include "nix/store/build-result.hh" +#include "nix/util/callback.hh" +#include "nix/store/realisation.hh" + +namespace nix { + +static StorePath pathPartOfReq(const SingleDerivedPath & req) +{ + return std::visit( + overloaded{ + [&](const SingleDerivedPath::Opaque & bo) { return bo.path; }, + [&](const SingleDerivedPath::Built & bfd) { return pathPartOfReq(*bfd.drvPath); }, + }, + req.raw()); +} + +static StorePath pathPartOfReq(const DerivedPath & req) +{ + return std::visit( + overloaded{ + [&](const DerivedPath::Opaque & bo) { return bo.path; }, + [&](const DerivedPath::Built & bfd) { return pathPartOfReq(*bfd.drvPath); }, + }, + req.raw()); +} + +bool RestrictionContext::isAllowed(const DerivedPath & req) +{ + return isAllowed(pathPartOfReq(req)); +} + +/** + * A wrapper around LocalStore that only allows building/querying of + * paths that are in the input closures of the build or were added via + * recursive Nix calls. + */ +struct RestrictedStore : public virtual IndirectRootStore, public virtual GcStore +{ + ref config; + + ref next; + + RestrictionContext & goal; + + RestrictedStore(ref config, ref next, RestrictionContext & goal) + : Store{*config} + , LocalFSStore{*config} + , config{config} + , next(next) + , goal(goal) + { + } + + Path getRealStoreDir() override + { + return next->config->realStoreDir; + } + + std::string getUri() override + { + return next->getUri(); + } + + StorePathSet queryAllValidPaths() override; + + void queryPathInfoUncached( + const StorePath & path, Callback> callback) noexcept override; + + void queryReferrers(const StorePath & path, StorePathSet & referrers) override; + + std::map> + queryPartialDerivationOutputMap(const StorePath & path, Store * evalStore = nullptr) override; + + std::optional queryPathFromHashPart(const std::string & hashPart) override + { + throw Error("queryPathFromHashPart"); + } + + StorePath addToStore( + std::string_view name, + const SourcePath & srcPath, + ContentAddressMethod method, + HashAlgorithm hashAlgo, + const StorePathSet & references, + PathFilter & filter, + RepairFlag repair) override + { + throw Error("addToStore"); + } + + void addToStore( + const ValidPathInfo & info, + Source & narSource, + RepairFlag repair = NoRepair, + CheckSigsFlag checkSigs = CheckSigs) override; + + StorePath addToStoreFromDump( + Source & dump, + std::string_view name, + FileSerialisationMethod dumpMethod, + ContentAddressMethod hashMethod, + HashAlgorithm hashAlgo, + const StorePathSet & references, + RepairFlag repair) override; + + void narFromPath(const StorePath & path, Sink & sink) override; + + void ensurePath(const StorePath & path) override; + + void registerDrvOutput(const Realisation & info) override; + + void queryRealisationUncached( + const DrvOutput & id, Callback> callback) noexcept override; + + void + buildPaths(const std::vector & paths, BuildMode buildMode, std::shared_ptr evalStore) override; + + std::vector buildPathsWithResults( + const std::vector & paths, + BuildMode buildMode = bmNormal, + std::shared_ptr evalStore = nullptr) override; + + BuildResult + buildDerivation(const StorePath & drvPath, const BasicDerivation & drv, BuildMode buildMode = bmNormal) override + { + unsupported("buildDerivation"); + } + + void addTempRoot(const StorePath & path) override {} + + void addIndirectRoot(const Path & path) override {} + + Roots findRoots(bool censor) override + { + return Roots(); + } + + void collectGarbage(const GCOptions & options, GCResults & results) override {} + + void addSignatures(const StorePath & storePath, const StringSet & sigs) override + { + unsupported("addSignatures"); + } + + MissingPaths queryMissing(const std::vector & targets) override; + + virtual std::optional getBuildLogExact(const StorePath & path) override + { + return std::nullopt; + } + + virtual void addBuildLog(const StorePath & path, std::string_view log) override + { + unsupported("addBuildLog"); + } + + std::optional isTrustedClient() override + { + return NotTrusted; + } +}; + +ref makeRestrictedStore(ref config, ref next, RestrictionContext & context) +{ + return make_ref(config, next, context); +} + +StorePathSet RestrictedStore::queryAllValidPaths() +{ + StorePathSet paths; + for (auto & p : goal.originalPaths()) + paths.insert(p); + for (auto & p : goal.addedPaths) + paths.insert(p); + return paths; +} + +void RestrictedStore::queryPathInfoUncached( + const StorePath & path, Callback> callback) noexcept +{ + if (goal.isAllowed(path)) { + try { + /* Censor impure information. */ + auto info = std::make_shared(*next->queryPathInfo(path)); + info->deriver.reset(); + info->registrationTime = 0; + info->ultimate = false; + info->sigs.clear(); + callback(info); + } catch (InvalidPath &) { + callback(nullptr); + } + } else + callback(nullptr); +}; + +void RestrictedStore::queryReferrers(const StorePath & path, StorePathSet & referrers) {} + +std::map> +RestrictedStore::queryPartialDerivationOutputMap(const StorePath & path, Store * evalStore) +{ + if (!goal.isAllowed(path)) + throw InvalidPath("cannot query output map for unknown path '%s' in recursive Nix", printStorePath(path)); + return next->queryPartialDerivationOutputMap(path, evalStore); +} + +void RestrictedStore::addToStore( + const ValidPathInfo & info, Source & narSource, RepairFlag repair, CheckSigsFlag checkSigs) +{ + next->addToStore(info, narSource, repair, checkSigs); + goal.addDependency(info.path); +} + +StorePath RestrictedStore::addToStoreFromDump( + Source & dump, + std::string_view name, + FileSerialisationMethod dumpMethod, + ContentAddressMethod hashMethod, + HashAlgorithm hashAlgo, + const StorePathSet & references, + RepairFlag repair) +{ + auto path = next->addToStoreFromDump(dump, name, dumpMethod, hashMethod, hashAlgo, references, repair); + goal.addDependency(path); + return path; +} + +void RestrictedStore::narFromPath(const StorePath & path, Sink & sink) +{ + if (!goal.isAllowed(path)) + throw InvalidPath("cannot dump unknown path '%s' in recursive Nix", printStorePath(path)); + LocalFSStore::narFromPath(path, sink); +} + +void RestrictedStore::ensurePath(const StorePath & path) +{ + if (!goal.isAllowed(path)) + throw InvalidPath("cannot substitute unknown path '%s' in recursive Nix", printStorePath(path)); + /* Nothing to be done; 'path' must already be valid. */ +} + +void RestrictedStore::registerDrvOutput(const Realisation & info) +// XXX: This should probably be allowed as a no-op if the realisation +// corresponds to an allowed derivation +{ + throw Error("registerDrvOutput"); +} + +void RestrictedStore::queryRealisationUncached( + const DrvOutput & id, Callback> callback) noexcept +// XXX: This should probably be allowed if the realisation corresponds to +// an allowed derivation +{ + if (!goal.isAllowed(id)) + callback(nullptr); + next->queryRealisation(id, std::move(callback)); +} + +void RestrictedStore::buildPaths( + const std::vector & paths, BuildMode buildMode, std::shared_ptr evalStore) +{ + for (auto & result : buildPathsWithResults(paths, buildMode, evalStore)) + if (!result.success()) + result.rethrow(); +} + +std::vector RestrictedStore::buildPathsWithResults( + const std::vector & paths, BuildMode buildMode, std::shared_ptr evalStore) +{ + assert(!evalStore); + + if (buildMode != bmNormal) + throw Error("unsupported build mode"); + + StorePathSet newPaths; + std::set newRealisations; + + for (auto & req : paths) { + if (!goal.isAllowed(req)) + throw InvalidPath("cannot build '%s' in recursive Nix because path is unknown", req.to_string(*next)); + } + + auto results = next->buildPathsWithResults(paths, buildMode); + + for (auto & result : results) { + for (auto & [outputName, output] : result.builtOutputs) { + newPaths.insert(output.outPath); + newRealisations.insert(output); + } + } + + StorePathSet closure; + next->computeFSClosure(newPaths, closure); + for (auto & path : closure) + goal.addDependency(path); + for (auto & real : Realisation::closure(*next, newRealisations)) + goal.addedDrvOutputs.insert(real.id); + + return results; +} + +MissingPaths RestrictedStore::queryMissing(const std::vector & targets) +{ + /* This is slightly impure since it leaks information to the + client about what paths will be built/substituted or are + already present. Probably not a big deal. */ + + std::vector allowed; + StorePathSet unknown; + for (auto & req : targets) { + if (goal.isAllowed(req)) + allowed.emplace_back(req); + else + unknown.insert(pathPartOfReq(req)); + } + + auto res = next->queryMissing(allowed); + + for (auto & p : unknown) + res.unknown.insert(p); + + return res; +} + +} diff --git a/src/libstore/s3-binary-cache-store.cc b/src/libstore/s3-binary-cache-store.cc index bf351a56d31..cbb47c063f5 100644 --- a/src/libstore/s3-binary-cache-store.cc +++ b/src/libstore/s3-binary-cache-store.cc @@ -1,15 +1,17 @@ -#if ENABLE_S3 +#include "nix/store/s3-binary-cache-store.hh" + +#if NIX_WITH_S3_SUPPORT #include -#include "s3.hh" -#include "s3-binary-cache-store.hh" -#include "nar-info.hh" -#include "nar-info-disk-cache.hh" -#include "globals.hh" -#include "compression.hh" -#include "filetransfer.hh" -#include "signals.hh" +#include "nix/store/s3.hh" +#include "nix/store/nar-info.hh" +#include "nix/store/nar-info-disk-cache.hh" +#include "nix/store/globals.hh" +#include "nix/util/compression.hh" +#include "nix/store/filetransfer.hh" +#include "nix/util/signals.hh" +#include "nix/store/store-registration.hh" #include #include @@ -20,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -34,10 +37,11 @@ namespace nix { struct S3Error : public Error { Aws::S3::S3Errors err; + Aws::String exceptionName; template - S3Error(Aws::S3::S3Errors err, const Args & ... args) - : Error(args...), err(err) { }; + S3Error(Aws::S3::S3Errors err, Aws::String exceptionName, const Args & ... args) + : Error(args...), err(err), exceptionName(exceptionName) { }; }; /* Helper: given an Outcome, return R in case of success, or @@ -48,7 +52,12 @@ R && checkAws(std::string_view s, Aws::Utils::Outcome && outcome) if (!outcome.IsSuccess()) throw S3Error( outcome.GetError().GetErrorType(), - s + ": " + outcome.GetError().GetMessage()); + outcome.GetError().GetExceptionName(), + fmt( + "%s: %s (request id: %s)", + s, + outcome.GetError().GetMessage(), + outcome.GetError().GetRequestId())); return outcome.GetResultWithOwnership(); } @@ -66,6 +75,29 @@ class AwsLogger : public Aws::Utils::Logging::FormattedLogSystem #endif }; +/* Retrieve the credentials from the list of AWS default providers, with the addition of the STS creds provider. This + last can be used to acquire further permissions with a specific IAM role. + Roughly based on https://github.com/aws/aws-sdk-cpp/issues/150#issuecomment-538548438 +*/ +struct CustomAwsCredentialsProviderChain : public Aws::Auth::AWSCredentialsProviderChain +{ + CustomAwsCredentialsProviderChain(const std::string & profile) + { + if (profile.empty()) { + // Use all the default AWS providers, plus the possibility to acquire a IAM role directly via a profile. + Aws::Auth::DefaultAWSCredentialsProviderChain default_aws_chain; + for (auto provider : default_aws_chain.GetProviders()) + AddProvider(provider); + AddProvider(std::make_shared()); + } else { + // Override the profile name to retrieve from the AWS config and credentials. I believe this option + // comes from the ?profile querystring in nix.conf. + AddProvider(std::make_shared(profile.c_str())); + AddProvider(std::make_shared(profile)); + } + } +}; + static void initAWS() { static std::once_flag flag; @@ -97,13 +129,8 @@ S3Helper::S3Helper( const std::string & endpoint) : config(makeConfig(region, scheme, endpoint)) , client(make_ref( - profile == "" - ? std::dynamic_pointer_cast( - std::make_shared()) - : std::dynamic_pointer_cast( - std::make_shared(profile.c_str())), + std::make_shared(profile), *config, - // FIXME: https://github.com/aws/aws-sdk-cpp/issues/759 #if AWS_SDK_VERSION_MAJOR == 1 && AWS_SDK_VERSION_MINOR < 3 false, #else @@ -121,9 +148,10 @@ class RetryStrategy : public Aws::Client::DefaultRetryStrategy checkInterrupt(); auto retry = Aws::Client::DefaultRetryStrategy::ShouldRetry(error, attemptedRetries); if (retry) - printError("AWS error '%s' (%s), will retry in %d ms", + printError("AWS error '%s' (%s; request id: %s), will retry in %d ms", error.GetExceptionName(), error.GetMessage(), + error.GetRequestId(), CalculateDelayBeforeNextRetry(error, attemptedRetries)); return retry; } @@ -154,7 +182,10 @@ ref S3Helper::makeConfig( S3Helper::FileTransferResult S3Helper::getObject( const std::string & bucketName, const std::string & key) { - debug("fetching 's3://%s/%s'...", bucketName, key); + std::string uri = "s3://" + bucketName + "/" + key; + Activity act(*logger, lvlTalkative, actFileTransfer, + fmt("downloading '%s'", uri), + Logger::Fields{uri}, getCurActivity()); auto request = Aws::S3::Model::GetObjectRequest() @@ -165,6 +196,22 @@ S3Helper::FileTransferResult S3Helper::getObject( return Aws::New("STRINGSTREAM"); }); + size_t bytesDone = 0; + size_t bytesExpected = 0; + request.SetDataReceivedEventHandler([&](const Aws::Http::HttpRequest * req, Aws::Http::HttpResponse * resp, long long l) { + if (!bytesExpected && resp->HasHeader("Content-Length")) { + if (auto length = string2Int(resp->GetHeader("Content-Length"))) { + bytesExpected = *length; + } + } + bytesDone += l; + act.progress(bytesDone, bytesExpected); + }); + + request.SetContinueRequestHandler([](const Aws::Http::HttpRequest*) { + return !isInterrupted(); + }); + FileTransferResult res; auto now1 = std::chrono::steady_clock::now(); @@ -174,12 +221,20 @@ S3Helper::FileTransferResult S3Helper::getObject( auto result = checkAws(fmt("AWS error fetching '%s'", key), client->GetObject(request)); + act.progress(result.GetContentLength(), result.GetContentLength()); + res.data = decompress(result.GetContentEncoding(), dynamic_cast(result.GetBody()).str()); } catch (S3Error & e) { if ((e.err != Aws::S3::S3Errors::NO_SUCH_KEY) && - (e.err != Aws::S3::S3Errors::ACCESS_DENIED)) throw; + (e.err != Aws::S3::S3Errors::ACCESS_DENIED) && + // Expired tokens are not really an error, more of a caching problem. Should be treated same as 403. + // + // AWS unwilling to provide a specific error type for the situation (https://github.com/aws/aws-sdk-cpp/issues/1843) + // so use this hack + (e.exceptionName != "ExpiredToken") + ) throw; } auto now2 = std::chrono::steady_clock::now(); @@ -189,11 +244,6 @@ S3Helper::FileTransferResult S3Helper::getObject( return res; } -S3BinaryCacheStore::S3BinaryCacheStore(const Params & params) - : BinaryCacheStoreConfig(params) - , BinaryCacheStore(params) -{ } - S3BinaryCacheStoreConfig::S3BinaryCacheStoreConfig( std::string_view uriScheme, @@ -212,6 +262,12 @@ S3BinaryCacheStoreConfig::S3BinaryCacheStoreConfig( throw UsageError("`%s` store requires a bucket name in its Store URI", uriScheme); } + +S3BinaryCacheStore::S3BinaryCacheStore(ref config) + : BinaryCacheStore(*config) + , config{config} +{ } + std::string S3BinaryCacheStoreConfig::doc() { return @@ -220,40 +276,37 @@ std::string S3BinaryCacheStoreConfig::doc() } -struct S3BinaryCacheStoreImpl : virtual S3BinaryCacheStoreConfig, public virtual S3BinaryCacheStore +struct S3BinaryCacheStoreImpl : virtual S3BinaryCacheStore { Stats stats; S3Helper s3Helper; - S3BinaryCacheStoreImpl( - std::string_view uriScheme, - std::string_view bucketName, - const Params & params) - : StoreConfig(params) - , BinaryCacheStoreConfig(params) - , S3BinaryCacheStoreConfig(uriScheme, bucketName, params) - , Store(params) - , BinaryCacheStore(params) - , S3BinaryCacheStore(params) - , s3Helper(profile, region, scheme, endpoint) + S3BinaryCacheStoreImpl(ref config) + : Store{*config} + , BinaryCacheStore{*config} + , S3BinaryCacheStore{config} + , s3Helper(config->profile, config->region, config->scheme, config->endpoint) { diskCache = getNarInfoDiskCache(); + + init(); } std::string getUri() override { - return "s3://" + bucketName; + return "s3://" + config->bucketName; } void init() override { if (auto cacheInfo = diskCache->upToDateCacheExists(getUri())) { - wantMassQuery.setDefault(cacheInfo->wantMassQuery); - priority.setDefault(cacheInfo->priority); + config->wantMassQuery.setDefault(cacheInfo->wantMassQuery); + config->priority.setDefault(cacheInfo->priority); } else { BinaryCacheStore::init(); - diskCache->createCache(getUri(), storeDir, wantMassQuery, priority); + diskCache->createCache( + getUri(), config->storeDir, config->wantMassQuery, config->priority); } } @@ -282,13 +335,17 @@ struct S3BinaryCacheStoreImpl : virtual S3BinaryCacheStoreConfig, public virtual auto res = s3Helper.client->HeadObject( Aws::S3::Model::HeadObjectRequest() - .WithBucket(bucketName) + .WithBucket(config->bucketName) .WithKey(path)); if (!res.IsSuccess()) { auto & error = res.GetError(); if (error.GetErrorType() == Aws::S3::S3Errors::RESOURCE_NOT_FOUND || error.GetErrorType() == Aws::S3::S3Errors::NO_SUCH_KEY + // Expired tokens are not really an error, more of a caching problem. Should be treated same as 403. + // AWS unwilling to provide a specific error type for the situation (https://github.com/aws/aws-sdk-cpp/issues/1843) + // so use this hack + || (error.GetErrorType() == Aws::S3::S3Errors::UNKNOWN && error.GetExceptionName() == "ExpiredToken") // If bucket listing is disabled, 404s turn into 403s || error.GetErrorType() == Aws::S3::S3Errors::ACCESS_DENIED) return false; @@ -301,11 +358,35 @@ struct S3BinaryCacheStoreImpl : virtual S3BinaryCacheStoreConfig, public virtual std::shared_ptr transferManager; std::once_flag transferManagerCreated; + struct AsyncContext : public Aws::Client::AsyncCallerContext + { + mutable std::mutex mutex; + mutable std::condition_variable cv; + const Activity & act; + + void notify() const + { + cv.notify_one(); + } + + void wait() const + { + std::unique_lock lk(mutex); + cv.wait(lk); + } + + AsyncContext(const Activity & act) : act(act) {} + }; + void uploadFile(const std::string & path, std::shared_ptr> istream, const std::string & mimeType, const std::string & contentEncoding) { + std::string uri = "s3://" + config->bucketName + "/" + path; + Activity act(*logger, lvlTalkative, actFileTransfer, + fmt("uploading '%s'", uri), + Logger::Fields{uri}, getCurActivity()); istream->seekg(0, istream->end); auto size = istream->tellg(); istream->seekg(0, istream->beg); @@ -317,23 +398,32 @@ struct S3BinaryCacheStoreImpl : virtual S3BinaryCacheStoreConfig, public virtual std::call_once(transferManagerCreated, [&]() { - if (multipartUpload) { + if (config->multipartUpload) { TransferManagerConfiguration transferConfig(executor.get()); transferConfig.s3Client = s3Helper.client; - transferConfig.bufferSize = bufferSize; + transferConfig.bufferSize = config->bufferSize; transferConfig.uploadProgressCallback = - [](const TransferManager *transferManager, - const std::shared_ptr - &transferHandle) + [](const TransferManager * transferManager, + const std::shared_ptr & transferHandle) { - //FIXME: find a way to properly abort the multipart upload. - //checkInterrupt(); - debug("upload progress ('%s'): '%d' of '%d' bytes", - transferHandle->GetKey(), - transferHandle->GetBytesTransferred(), - transferHandle->GetBytesTotalSize()); + auto context = std::dynamic_pointer_cast(transferHandle->GetContext()); + size_t bytesDone = transferHandle->GetBytesTransferred(); + size_t bytesTotal = transferHandle->GetBytesTotalSize(); + try { + checkInterrupt(); + context->act.progress(bytesDone, bytesTotal); + } catch (...) { + context->notify(); + } + }; + transferConfig.transferStatusUpdatedCallback = + [](const TransferManager * transferManager, + const std::shared_ptr & transferHandle) + { + auto context = std::dynamic_pointer_cast(transferHandle->GetContext()); + context->notify(); }; transferManager = TransferManager::Create(transferConfig); @@ -342,34 +432,58 @@ struct S3BinaryCacheStoreImpl : virtual S3BinaryCacheStoreConfig, public virtual auto now1 = std::chrono::steady_clock::now(); + auto & bucketName = config->bucketName; + if (transferManager) { if (contentEncoding != "") throw Error("setting a content encoding is not supported with S3 multi-part uploads"); + auto context = std::make_shared(act); std::shared_ptr transferHandle = transferManager->UploadFile( istream, bucketName, path, mimeType, Aws::Map(), - nullptr /*, contentEncoding */); - - transferHandle->WaitUntilFinished(); + context /*, contentEncoding */); + + TransferStatus status = transferHandle->GetStatus(); + while (status == TransferStatus::IN_PROGRESS || status == TransferStatus::NOT_STARTED) { + if (!isInterrupted()) { + context->wait(); + } else { + transferHandle->Cancel(); + transferHandle->WaitUntilFinished(); + } + status = transferHandle->GetStatus(); + } + act.progress(transferHandle->GetBytesTransferred(), transferHandle->GetBytesTotalSize()); - if (transferHandle->GetStatus() == TransferStatus::FAILED) + if (status == TransferStatus::FAILED) throw Error("AWS error: failed to upload 's3://%s/%s': %s", bucketName, path, transferHandle->GetLastError().GetMessage()); - if (transferHandle->GetStatus() != TransferStatus::COMPLETED) + if (status != TransferStatus::COMPLETED) throw Error("AWS error: transfer status of 's3://%s/%s' in unexpected state", bucketName, path); } else { + act.progress(0, size); auto request = Aws::S3::Model::PutObjectRequest() .WithBucket(bucketName) .WithKey(path); + size_t bytesSent = 0; + request.SetDataSentEventHandler([&](const Aws::Http::HttpRequest * req, long long l) { + bytesSent += l; + act.progress(bytesSent, size); + }); + + request.SetContinueRequestHandler([](const Aws::Http::HttpRequest*) { + return !isInterrupted(); + }); + request.SetContentType(mimeType); if (contentEncoding != "") @@ -379,6 +493,8 @@ struct S3BinaryCacheStoreImpl : virtual S3BinaryCacheStoreConfig, public virtual auto result = checkAws(fmt("AWS error uploading '%s'", path), s3Helper.client->PutObject(request)); + + act.progress(size, size); } auto now2 = std::chrono::steady_clock::now(); @@ -405,12 +521,12 @@ struct S3BinaryCacheStoreImpl : virtual S3BinaryCacheStoreConfig, public virtual return std::make_shared(std::move(compressed)); }; - if (narinfoCompression != "" && hasSuffix(path, ".narinfo")) - uploadFile(path, compress(narinfoCompression), mimeType, narinfoCompression); - else if (lsCompression != "" && hasSuffix(path, ".ls")) - uploadFile(path, compress(lsCompression), mimeType, lsCompression); - else if (logCompression != "" && hasPrefix(path, "log/")) - uploadFile(path, compress(logCompression), mimeType, logCompression); + if (config->narinfoCompression != "" && hasSuffix(path, ".narinfo")) + uploadFile(path, compress(config->narinfoCompression), mimeType, config->narinfoCompression); + else if (config->lsCompression != "" && hasSuffix(path, ".ls")) + uploadFile(path, compress(config->lsCompression), mimeType, config->lsCompression); + else if (config->logCompression != "" && hasPrefix(path, "log/")) + uploadFile(path, compress(config->logCompression), mimeType, config->logCompression); else uploadFile(path, istream, mimeType, ""); } @@ -420,14 +536,14 @@ struct S3BinaryCacheStoreImpl : virtual S3BinaryCacheStoreConfig, public virtual stats.get++; // FIXME: stream output to sink. - auto res = s3Helper.getObject(bucketName, path); + auto res = s3Helper.getObject(config->bucketName, path); stats.getBytes += res.data ? res.data->size() : 0; stats.getTimeMs += res.durationMs; if (res.data) { printTalkative("downloaded 's3://%s/%s' (%d bytes) in %d ms", - bucketName, path, res.data->size(), res.durationMs); + config->bucketName, path, res.data->size(), res.durationMs); sink(*res.data); } else @@ -439,6 +555,8 @@ struct S3BinaryCacheStoreImpl : virtual S3BinaryCacheStoreConfig, public virtual StorePathSet paths; std::string marker; + auto & bucketName = config->bucketName; + do { debug("listing bucket 's3://%s' from key '%s'...", bucketName, marker); @@ -477,7 +595,15 @@ struct S3BinaryCacheStoreImpl : virtual S3BinaryCacheStoreConfig, public virtual } }; -static RegisterStoreImplementation regS3BinaryCacheStore; +ref S3BinaryCacheStoreImpl::Config::openStore() const +{ + return make_ref(ref{ + // FIXME we shouldn't actually need a mutable config + std::const_pointer_cast(shared_from_this()) + }); +} + +static RegisterStoreImplementation regS3BinaryCacheStore; } diff --git a/src/libstore/serve-protocol-connection.cc b/src/libstore/serve-protocol-connection.cc index 07379999b4b..276086f6f31 100644 --- a/src/libstore/serve-protocol-connection.cc +++ b/src/libstore/serve-protocol-connection.cc @@ -1,7 +1,7 @@ -#include "serve-protocol-connection.hh" -#include "serve-protocol-impl.hh" -#include "build-result.hh" -#include "derivations.hh" +#include "nix/store/serve-protocol-connection.hh" +#include "nix/store/serve-protocol-impl.hh" +#include "nix/store/build-result.hh" +#include "nix/store/derivations.hh" namespace nix { diff --git a/src/libstore/serve-protocol.cc b/src/libstore/serve-protocol.cc index 08bfad9e405..520c3795193 100644 --- a/src/libstore/serve-protocol.cc +++ b/src/libstore/serve-protocol.cc @@ -1,11 +1,11 @@ -#include "serialise.hh" -#include "path-with-outputs.hh" -#include "store-api.hh" -#include "build-result.hh" -#include "serve-protocol.hh" -#include "serve-protocol-impl.hh" -#include "archive.hh" -#include "path-info.hh" +#include "nix/util/serialise.hh" +#include "nix/store/path-with-outputs.hh" +#include "nix/store/store-api.hh" +#include "nix/store/build-result.hh" +#include "nix/store/serve-protocol.hh" +#include "nix/store/serve-protocol-impl.hh" +#include "nix/util/archive.hh" +#include "nix/store/path-info.hh" #include diff --git a/src/libstore/sqlite.cc b/src/libstore/sqlite.cc index f02e472fd5f..55b967ed679 100644 --- a/src/libstore/sqlite.cc +++ b/src/libstore/sqlite.cc @@ -1,8 +1,8 @@ -#include "sqlite.hh" -#include "globals.hh" -#include "util.hh" -#include "url.hh" -#include "signals.hh" +#include "nix/store/sqlite.hh" +#include "nix/store/globals.hh" +#include "nix/util/util.hh" +#include "nix/util/url.hh" +#include "nix/util/signals.hh" #include diff --git a/src/libstore/ssh-store.cc b/src/libstore/ssh-store.cc index 954a9746774..6992ae77462 100644 --- a/src/libstore/ssh-store.cc +++ b/src/libstore/ssh-store.cc @@ -1,12 +1,13 @@ -#include "ssh-store.hh" -#include "local-fs-store.hh" -#include "remote-store-connection.hh" -#include "source-accessor.hh" -#include "archive.hh" -#include "worker-protocol.hh" -#include "worker-protocol-impl.hh" -#include "pool.hh" -#include "ssh.hh" +#include "nix/store/ssh-store.hh" +#include "nix/store/local-fs-store.hh" +#include "nix/store/remote-store-connection.hh" +#include "nix/util/source-accessor.hh" +#include "nix/util/archive.hh" +#include "nix/store/worker-protocol.hh" +#include "nix/store/worker-protocol-impl.hh" +#include "nix/util/pool.hh" +#include "nix/store/ssh.hh" +#include "nix/store/store-registration.hh" namespace nix { @@ -14,12 +15,13 @@ SSHStoreConfig::SSHStoreConfig( std::string_view scheme, std::string_view authority, const Params & params) - : StoreConfig(params) - , RemoteStoreConfig(params) - , CommonSSHStoreConfig(scheme, authority, params) + : Store::Config{params} + , RemoteStore::Config{params} + , CommonSSHStoreConfig{scheme, authority, params} { } + std::string SSHStoreConfig::doc() { return @@ -27,21 +29,18 @@ std::string SSHStoreConfig::doc() ; } -class SSHStore : public virtual SSHStoreConfig, public virtual RemoteStore + +struct SSHStore : virtual RemoteStore { -public: - - SSHStore( - std::string_view scheme, - std::string_view host, - const Params & params) - : StoreConfig(params) - , RemoteStoreConfig(params) - , CommonSSHStoreConfig(scheme, host, params) - , SSHStoreConfig(scheme, host, params) - , Store(params) - , RemoteStore(params) - , master(createSSHMaster( + using Config = SSHStoreConfig; + + ref config; + + SSHStore(ref config) + : Store{*config} + , RemoteStore{*config} + , config{config} + , master(config->createSSHMaster( // Use SSH master only if using more than 1 connection. connections->capacity() > 1)) { @@ -49,7 +48,7 @@ class SSHStore : public virtual SSHStoreConfig, public virtual RemoteStore std::string getUri() override { - return *uriSchemes().begin() + "://" + host; + return *Config::uriSchemes().begin() + "://" + host; } // FIXME extend daemon protocol, move implementation to RemoteStore @@ -101,7 +100,7 @@ MountedSSHStoreConfig::MountedSSHStoreConfig(std::string_view scheme, std::strin : StoreConfig(params) , RemoteStoreConfig(params) , CommonSSHStoreConfig(scheme, host, params) - , SSHStoreConfig(params) + , SSHStoreConfig(scheme, host, params) , LocalFSStoreConfig(params) { } @@ -121,42 +120,28 @@ std::string MountedSSHStoreConfig::doc() * store. * * MountedSSHStore is very similar to UDSRemoteStore --- ignoring the - * superficial differnce of SSH vs Unix domain sockets, they both are + * superficial difference of SSH vs Unix domain sockets, they both are * accessing remote stores, and they both assume the store will be * mounted in the local filesystem. * * The difference lies in how they manage GC roots. See addPermRoot * below for details. */ -class MountedSSHStore : public virtual MountedSSHStoreConfig, public virtual SSHStore, public virtual LocalFSStore +struct MountedSSHStore : virtual SSHStore, virtual LocalFSStore { -public: - - MountedSSHStore( - std::string_view scheme, - std::string_view host, - const Params & params) - : StoreConfig(params) - , RemoteStoreConfig(params) - , CommonSSHStoreConfig(scheme, host, params) - , SSHStoreConfig(params) - , LocalFSStoreConfig(params) - , MountedSSHStoreConfig(params) - , Store(params) - , RemoteStore(params) - , SSHStore(scheme, host, params) - , LocalFSStore(params) + using Config = MountedSSHStoreConfig; + + MountedSSHStore(ref config) + : Store{*config} + , RemoteStore{*config} + , SSHStore{config} + , LocalFSStore{*config} { extraRemoteProgramArgs = { "--process-ops", }; } - std::string getUri() override - { - return *uriSchemes().begin() + "://" + host; - } - void narFromPath(const StorePath & path, Sink & sink) override { return LocalFSStore::narFromPath(path, sink); @@ -198,14 +183,26 @@ class MountedSSHStore : public virtual MountedSSHStoreConfig, public virtual SSH } }; + +ref SSHStore::Config::openStore() const { + return make_ref(ref{shared_from_this()}); +} + +ref MountedSSHStore::Config::openStore() const { + return make_ref(ref{ + std::dynamic_pointer_cast(shared_from_this()) + }); +} + + ref SSHStore::openConnection() { auto conn = make_ref(); - Strings command = remoteProgram.get(); + Strings command = config->remoteProgram.get(); command.push_back("--stdio"); - if (remoteStore.get() != "") { + if (config->remoteStore.get() != "") { command.push_back("--store"); - command.push_back(remoteStore.get()); + command.push_back(config->remoteStore.get()); } command.insert(command.end(), extraRemoteProgramArgs.begin(), extraRemoteProgramArgs.end()); @@ -215,7 +212,7 @@ ref SSHStore::openConnection() return conn; } -static RegisterStoreImplementation regSSHStore; -static RegisterStoreImplementation regMountedSSHStore; +static RegisterStoreImplementation regSSHStore; +static RegisterStoreImplementation regMountedSSHStore; } diff --git a/src/libstore/ssh.cc b/src/libstore/ssh.cc index dec733fd516..c8fec52442e 100644 --- a/src/libstore/ssh.cc +++ b/src/libstore/ssh.cc @@ -1,9 +1,9 @@ -#include "ssh.hh" -#include "finally.hh" -#include "current-process.hh" -#include "environment-variables.hh" -#include "util.hh" -#include "exec.hh" +#include "nix/store/ssh.hh" +#include "nix/util/finally.hh" +#include "nix/util/current-process.hh" +#include "nix/util/environment-variables.hh" +#include "nix/util/util.hh" +#include "nix/util/exec.hh" namespace nix { @@ -34,15 +34,24 @@ SSHMaster::SSHMaster( throw Error("invalid SSH host name '%s'", host); auto state(state_.lock()); - state->tmpDir = std::make_unique(createTempDir("", "nix", true, true, 0700)); + state->tmpDir = std::make_unique(createTempDir("", "nix", 0700)); } void SSHMaster::addCommonSSHOpts(Strings & args) { auto state(state_.lock()); - for (auto & i : tokenizeString(getEnv("NIX_SSHOPTS").value_or(""))) - args.push_back(i); + std::string sshOpts = getEnv("NIX_SSHOPTS").value_or(""); + + try { + std::list opts = shellSplitString(sshOpts); + for (auto & i : opts) + args.push_back(i); + } catch (Error & e) { + e.addTrace({}, "while splitting NIX_SSHOPTS '%s'", sshOpts); + throw; + } + if (!keyFile.empty()) args.insert(args.end(), {"-i", keyFile}); if (!sshPublicHostKey.empty()) { @@ -74,7 +83,7 @@ bool SSHMaster::isMasterRunning() { Strings createSSHEnv() { // Copy the environment and set SHELL=/bin/sh - std::map env = getEnv(); + StringMap env = getEnv(); // SSH will invoke the "user" shell for -oLocalCommand, but that means // $SHELL. To keep things simple and avoid potential issues with other @@ -108,10 +117,10 @@ std::unique_ptr SSHMaster::startCommand( ProcessOptions options; options.dieWithParent = false; + std::unique_ptr loggerSuspension; if (!fakeSSH && !useMaster) { - logger->pause(); + loggerSuspension = std::make_unique(logger->suspend()); } - Finally cleanup = [&]() { logger->resume(); }; conn->sshPid = startProcess([&]() { restoreProcessContext(); @@ -190,8 +199,7 @@ Path SSHMaster::startMaster() ProcessOptions options; options.dieWithParent = false; - logger->pause(); - Finally cleanup = [&]() { logger->resume(); }; + auto suspension = logger->suspend(); if (isMasterRunning()) return state->socketPath; @@ -231,4 +239,19 @@ Path SSHMaster::startMaster() #endif +void SSHMaster::Connection::trySetBufferSize(size_t size) +{ +#ifdef F_SETPIPE_SZ + /* This `fcntl` method of doing this takes a positive `int`. Check + and convert accordingly. + + The function overall still takes `size_t` because this is more + portable for a platform-agnostic interface. */ + assert(size <= INT_MAX); + int pipesize = size; + fcntl(in.get(), F_SETPIPE_SZ, pipesize); + fcntl(out.get(), F_SETPIPE_SZ, pipesize); +#endif +} + } diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index 78cc3b917d2..9aeab1d1f6b 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -1,41 +1,42 @@ -#include "signature/local-keys.hh" -#include "source-accessor.hh" -#include "globals.hh" -#include "derived-path.hh" -#include "realisation.hh" -#include "derivations.hh" -#include "store-api.hh" -#include "util.hh" -#include "nar-info-disk-cache.hh" -#include "thread-pool.hh" -#include "references.hh" -#include "archive.hh" -#include "callback.hh" -#include "git.hh" -#include "posix-source-accessor.hh" +#include "nix/util/signature/local-keys.hh" +#include "nix/util/source-accessor.hh" +#include "nix/store/globals.hh" +#include "nix/store/derived-path.hh" +#include "nix/store/realisation.hh" +#include "nix/store/derivations.hh" +#include "nix/store/store-api.hh" +#include "nix/store/store-open.hh" +#include "nix/util/util.hh" +#include "nix/store/nar-info-disk-cache.hh" +#include "nix/util/thread-pool.hh" +#include "nix/util/references.hh" +#include "nix/util/archive.hh" +#include "nix/util/callback.hh" +#include "nix/util/git.hh" +#include "nix/util/posix-source-accessor.hh" // FIXME this should not be here, see TODO below on // `addMultipleToStore`. -#include "worker-protocol.hh" -#include "signals.hh" -#include "users.hh" +#include "nix/store/worker-protocol.hh" +#include "nix/util/signals.hh" +#include "nix/util/users.hh" #include #include -#include "strings.hh" +#include "nix/util/strings.hh" using json = nlohmann::json; namespace nix { -bool StoreDirConfig::isInStore(PathView path) const +bool MixStoreDirMethods::isInStore(PathView path) const { return isInDir(path, storeDir); } -std::pair StoreDirConfig::toStorePath(PathView path) const +std::pair MixStoreDirMethods::toStorePath(PathView path) const { if (!isInStore(path)) throw Error("path '%1%' is not in the Nix store", path); @@ -77,7 +78,7 @@ to match. */ -StorePath StoreDirConfig::makeStorePath(std::string_view type, +StorePath MixStoreDirMethods::makeStorePath(std::string_view type, std::string_view hash, std::string_view name) const { /* e.g., "source:sha256:1abc...:/nix/store:foo.tar.gz" */ @@ -88,14 +89,14 @@ StorePath StoreDirConfig::makeStorePath(std::string_view type, } -StorePath StoreDirConfig::makeStorePath(std::string_view type, +StorePath MixStoreDirMethods::makeStorePath(std::string_view type, const Hash & hash, std::string_view name) const { return makeStorePath(type, hash.to_string(HashFormat::Base16, true), name); } -StorePath StoreDirConfig::makeOutputPath(std::string_view id, +StorePath MixStoreDirMethods::makeOutputPath(std::string_view id, const Hash & hash, std::string_view name) const { return makeStorePath("output:" + std::string { id }, hash, outputPathName(name, id)); @@ -106,7 +107,7 @@ StorePath StoreDirConfig::makeOutputPath(std::string_view id, hacky, but we can't put them in, say, (per the grammar above) since that would be ambiguous. */ static std::string makeType( - const StoreDirConfig & store, + const MixStoreDirMethods & store, std::string && type, const StoreReferences & references) { @@ -119,7 +120,7 @@ static std::string makeType( } -StorePath StoreDirConfig::makeFixedOutputPath(std::string_view name, const FixedOutputInfo & info) const +StorePath MixStoreDirMethods::makeFixedOutputPath(std::string_view name, const FixedOutputInfo & info) const { if (info.method == FileIngestionMethod::Git && info.hash.algo != HashAlgorithm::SHA1) throw Error("Git file ingestion must use SHA-1 hash"); @@ -141,7 +142,7 @@ StorePath StoreDirConfig::makeFixedOutputPath(std::string_view name, const Fixed } -StorePath StoreDirConfig::makeFixedOutputPathFromCA(std::string_view name, const ContentAddressWithReferences & ca) const +StorePath MixStoreDirMethods::makeFixedOutputPathFromCA(std::string_view name, const ContentAddressWithReferences & ca) const { // New template return std::visit(overloaded { @@ -162,7 +163,7 @@ StorePath StoreDirConfig::makeFixedOutputPathFromCA(std::string_view name, const } -std::pair StoreDirConfig::computeStorePath( +std::pair MixStoreDirMethods::computeStorePath( std::string_view name, const SourcePath & path, ContentAddressMethod method, @@ -223,32 +224,34 @@ StorePath Store::addToStore( } void Store::addMultipleToStore( - PathsSource & pathsToCopy, + PathsSource && pathsToCopy, Activity & act, RepairFlag repair, CheckSigsFlag checkSigs) { std::atomic nrDone{0}; std::atomic nrFailed{0}; - std::atomic bytesExpected{0}; std::atomic nrRunning{0}; using PathWithInfo = std::pair>; + uint64_t bytesExpected = 0; + std::map infosMap; StorePathSet storePathsToAdd; for (auto & thingToAdd : pathsToCopy) { + bytesExpected += thingToAdd.first.narSize; infosMap.insert_or_assign(thingToAdd.first.path, &thingToAdd); storePathsToAdd.insert(thingToAdd.first.path); } - auto showProgress = [&]() { - act.progress(nrDone, pathsToCopy.size(), nrRunning, nrFailed); - }; + act.setExpected(actCopyPath, bytesExpected); - ThreadPool pool; + auto showProgress = [&, nrTotal = pathsToCopy.size()]() { + act.progress(nrDone, nrTotal, nrRunning, nrFailed); + }; - processGraph(pool, + processGraph( storePathsToAdd, [&](const StorePath & path) { @@ -261,9 +264,6 @@ void Store::addMultipleToStore( return StorePathSet(); } - bytesExpected += info.narSize; - act.setExpected(actCopyPath, bytesExpected); - return info.references; }, @@ -333,10 +333,10 @@ digraph graphname { node [shape=box] fileSource -> narSink narSink [style=dashed] - narSink -> unsualHashTee [style = dashed, label = "Recursive && !SHA-256"] + narSink -> unusualHashTee [style = dashed, label = "Recursive && !SHA-256"] narSink -> narHashSink [style = dashed, label = "else"] - unsualHashTee -> narHashSink - unsualHashTee -> caHashSink + unusualHashTee -> narHashSink + unusualHashTee -> caHashSink fileSource -> parseSink parseSink [style=dashed] parseSink-> fileSink [style = dashed, label = "Flat"] @@ -421,7 +421,7 @@ ValidPathInfo Store::addToStoreSlow( return info; } -StringSet StoreConfig::getDefaultSystemFeatures() +StringSet Store::Config::getDefaultSystemFeatures() { auto res = settings.systemFeatures.get(); @@ -434,9 +434,10 @@ StringSet StoreConfig::getDefaultSystemFeatures() return res; } -Store::Store(const Params & params) - : StoreConfig(params) - , state({(size_t) pathInfoCacheSize}) +Store::Store(const Store::Config & config) + : MixStoreDirMethods{config} + , config{config} + , state({(size_t) config.pathInfoCacheSize}) { assertLibStoreInitialized(); } @@ -570,7 +571,7 @@ bool Store::isValidPath(const StorePath & storePath) { { auto state_(state.lock()); - auto res = state_->pathInfoCache.get(std::string(storePath.to_string())); + auto res = state_->pathInfoCache.get(storePath.to_string()); if (res && res->isKnownNow()) { stats.narInfoReadAverted++; return res->didExist(); @@ -582,7 +583,7 @@ bool Store::isValidPath(const StorePath & storePath) if (res.first != NarInfoDiskCache::oUnknown) { stats.narInfoReadAverted++; auto state_(state.lock()); - state_->pathInfoCache.upsert(std::string(storePath.to_string()), + state_->pathInfoCache.upsert(storePath.to_string(), res.first == NarInfoDiskCache::oInvalid ? PathInfoCacheValue{} : PathInfoCacheValue { .value = res.second }); return res.first == NarInfoDiskCache::oValid; } @@ -641,7 +642,7 @@ std::optional> Store::queryPathInfoFromClie auto hashPart = std::string(storePath.hashPart()); { - auto res = state.lock()->pathInfoCache.get(std::string(storePath.to_string())); + auto res = state.lock()->pathInfoCache.get(storePath.to_string()); if (res && res->isKnownNow()) { stats.narInfoReadAverted++; if (res->didExist()) @@ -657,7 +658,7 @@ std::optional> Store::queryPathInfoFromClie stats.narInfoReadAverted++; { auto state_(state.lock()); - state_->pathInfoCache.upsert(std::string(storePath.to_string()), + state_->pathInfoCache.upsert(storePath.to_string(), res.first == NarInfoDiskCache::oInvalid ? PathInfoCacheValue{} : PathInfoCacheValue{ .value = res.second }); if (res.first == NarInfoDiskCache::oInvalid || !goodStorePath(storePath, res.second->path)) @@ -701,7 +702,7 @@ void Store::queryPathInfo(const StorePath & storePath, { auto state_(state.lock()); - state_->pathInfoCache.upsert(std::string(storePath.to_string()), PathInfoCacheValue { .value = info }); + state_->pathInfoCache.upsert(storePath.to_string(), PathInfoCacheValue { .value = info }); } if (!info || !goodStorePath(storePath, info->path)) { @@ -789,15 +790,12 @@ void Store::substitutePaths(const StorePathSet & paths) for (auto & path : paths) if (!path.isDerivation()) paths2.emplace_back(DerivedPath::Opaque{path}); - uint64_t downloadSize, narSize; - StorePathSet willBuild, willSubstitute, unknown; - queryMissing(paths2, - willBuild, willSubstitute, unknown, downloadSize, narSize); + auto missing = queryMissing(paths2); - if (!willSubstitute.empty()) + if (!missing.willSubstitute.empty()) try { std::vector subs; - for (auto & p : willSubstitute) subs.emplace_back(DerivedPath::Opaque{p}); + for (auto & p : missing.willSubstitute) subs.emplace_back(DerivedPath::Opaque{p}); buildPaths(subs); } catch (Error & e) { logWarning(e.info()); @@ -1028,12 +1026,10 @@ std::map copyPaths( } auto pathsMap = copyPaths(srcStore, dstStore, storePaths, repair, checkSigs, substitute); - ThreadPool pool; - try { // Copy the realisation closure processGraph( - pool, Realisation::closure(srcStore, toplevelRealisations), + Realisation::closure(srcStore, toplevelRealisations), [&](const Realisation & current) -> std::set { std::set children; for (const auto & [drvOutput, _] : current.dependentRealisations) { @@ -1108,9 +1104,6 @@ std::map copyPaths( return storePathForDst; }; - // total is accessed by each copy, which are each handled in separate threads - std::atomic total = 0; - for (auto & missingPath : sortedMissing) { auto info = srcStore.queryPathInfo(missingPath); @@ -1120,9 +1113,10 @@ std::map copyPaths( ValidPathInfo infoForDst = *info; infoForDst.path = storePathForDst; - auto source = sinkToSource([&](Sink & sink) { + auto source = sinkToSource([&, narSize = info->narSize](Sink & sink) { // We can reasonably assume that the copy will happen whenever we // read the path, so log something about that at that point + uint64_t total = 0; auto srcUri = srcStore.getUri(); auto dstUri = dstStore.getUri(); auto storePathS = srcStore.printStorePath(missingPath); @@ -1133,16 +1127,16 @@ std::map copyPaths( LambdaSink progressSink([&](std::string_view data) { total += data.size(); - act.progress(total, info->narSize); + act.progress(total, narSize); }); TeeSink tee { sink, progressSink }; srcStore.narFromPath(missingPath, tee); }); - pathsToCopy.push_back(std::pair{infoForDst, std::move(source)}); + pathsToCopy.emplace_back(std::move(infoForDst), std::move(source)); } - dstStore.addMultipleToStore(pathsToCopy, act, repair, checkSigs); + dstStore.addMultipleToStore(std::move(pathsToCopy), act, repair, checkSigs); return pathsMap; } @@ -1210,7 +1204,7 @@ std::optional decodeValidPathInfo(const Store & store, std::istre } -std::string StoreDirConfig::showPaths(const StorePathSet & paths) +std::string MixStoreDirMethods::showPaths(const StorePathSet & paths) const { std::string s; for (auto & i : paths) { @@ -1238,7 +1232,7 @@ static Derivation readDerivationCommon(Store & store, const StorePath & drvPath, auto accessor = store.getFSAccessor(requireValidPath); try { return parseDerivation(store, - accessor->readFile(CanonPath(store.printStorePath(drvPath))), + accessor->readFile(CanonPath(drvPath.to_string())), Derivation::nameFromPath(drvPath)); } catch (FormatError & e) { throw Error("error parsing derivation '%s': %s", store.printStorePath(drvPath), e.msg()); @@ -1279,103 +1273,32 @@ Derivation Store::readDerivation(const StorePath & drvPath) Derivation Store::readInvalidDerivation(const StorePath & drvPath) { return readDerivationCommon(*this, drvPath, false); } -} - - -#include "local-store.hh" -#include "uds-remote-store.hh" - - -namespace nix { -ref openStore(const std::string & uri, - const Store::Params & extraParams) +void Store::signPathInfo(ValidPathInfo & info) { - return openStore(StoreReference::parse(uri, extraParams)); -} + // FIXME: keep secret keys in memory. -ref openStore(StoreReference && storeURI) -{ - auto & params = storeURI.params; - - auto store = std::visit(overloaded { - [&](const StoreReference::Auto &) -> std::shared_ptr { - auto stateDir = getOr(params, "state", settings.nixStateDir); - if (access(stateDir.c_str(), R_OK | W_OK) == 0) - return std::make_shared(params); - else if (pathExists(settings.nixDaemonSocketFile)) - return std::make_shared(params); - #if __linux__ - else if (!pathExists(stateDir) - && params.empty() - && !isRootUser() - && !getEnv("NIX_STORE_DIR").has_value() - && !getEnv("NIX_STATE_DIR").has_value()) - { - /* If /nix doesn't exist, there is no daemon socket, and - we're not root, then automatically set up a chroot - store in ~/.local/share/nix/root. */ - auto chrootStore = getDataDir() + "/root"; - if (!pathExists(chrootStore)) { - try { - createDirs(chrootStore); - } catch (SystemError & e) { - return std::make_shared(params); - } - warn("'%s' does not exist, so Nix will use '%s' as a chroot store", stateDir, chrootStore); - } else - debug("'%s' does not exist, so Nix will use '%s' as a chroot store", stateDir, chrootStore); - return std::make_shared("local", chrootStore, params); - } - #endif - else - return std::make_shared(params); - }, - [&](const StoreReference::Specified & g) { - for (const auto & implem : *Implementations::registered) - if (implem.uriSchemes.count(g.scheme)) - return implem.create(g.scheme, g.authority, params); - - throw Error("don't know how to open Nix store with scheme '%s'", g.scheme); - }, - }, storeURI.variant); - - experimentalFeatureSettings.require(store->experimentalFeature()); - store->warnUnknownSettings(); - store->init(); + auto secretKeyFiles = settings.secretKeyFiles; - return ref { store }; + for (auto & secretKeyFile : secretKeyFiles.get()) { + SecretKey secretKey(readFile(secretKeyFile)); + LocalSigner signer(std::move(secretKey)); + info.sign(*this, signer); + } } -std::list> getDefaultSubstituters() -{ - static auto stores([]() { - std::list> stores; - - StringSet done; - auto addStore = [&](const std::string & uri) { - if (!done.insert(uri).second) return; - try { - stores.push_back(openStore(uri)); - } catch (Error & e) { - logWarning(e.info()); - } - }; - - for (const auto & uri : settings.substituters.get()) - addStore(uri); - - stores.sort([](ref & a, ref & b) { - return a->priority < b->priority; - }); +void Store::signRealisation(Realisation & realisation) +{ + // FIXME: keep secret keys in memory. - return stores; - } ()); + auto secretKeyFiles = settings.secretKeyFiles; - return stores; + for (auto & secretKeyFile : secretKeyFiles.get()) { + SecretKey secretKey(readFile(secretKeyFile)); + LocalSigner signer(std::move(secretKey)); + realisation.sign(signer); + } } -std::vector * Implementations::registered = 0; - } diff --git a/src/libstore/store-dir-config.cc b/src/libstore/store-dir-config.cc new file mode 100644 index 00000000000..ec65013ef2a --- /dev/null +++ b/src/libstore/store-dir-config.cc @@ -0,0 +1,13 @@ +#include "nix/store/store-dir-config.hh" +#include "nix/util/util.hh" +#include "nix/store/globals.hh" + +namespace nix { + +StoreDirConfig::StoreDirConfig(const Params & params) + : StoreDirConfigBase(params) + , MixStoreDirMethods{storeDir_} +{ +} + +} diff --git a/src/libstore/store-reference.cc b/src/libstore/store-reference.cc index b4968dfadbd..cb4e2cfb8eb 100644 --- a/src/libstore/store-reference.cc +++ b/src/libstore/store-reference.cc @@ -1,10 +1,10 @@ #include -#include "error.hh" -#include "url.hh" -#include "store-reference.hh" -#include "file-system.hh" -#include "util.hh" +#include "nix/util/error.hh" +#include "nix/util/url.hh" +#include "nix/store/store-reference.hh" +#include "nix/util/file-system.hh" +#include "nix/util/util.hh" namespace nix { diff --git a/src/libstore/store-registration.cc b/src/libstore/store-registration.cc new file mode 100644 index 00000000000..6362ac0365b --- /dev/null +++ b/src/libstore/store-registration.cc @@ -0,0 +1,105 @@ +#include "nix/store/store-registration.hh" +#include "nix/store/store-open.hh" +#include "nix/store/local-store.hh" +#include "nix/store/uds-remote-store.hh" + +namespace nix { + +ref openStore(const std::string & uri, const Store::Config::Params & extraParams) +{ + return openStore(StoreReference::parse(uri, extraParams)); +} + +ref openStore(StoreReference && storeURI) +{ + auto store = resolveStoreConfig(std::move(storeURI))->openStore(); + store->init(); + return store; +} + +ref resolveStoreConfig(StoreReference && storeURI) +{ + auto & params = storeURI.params; + + auto storeConfig = std::visit( + overloaded{ + [&](const StoreReference::Auto &) -> ref { + auto stateDir = getOr(params, "state", settings.nixStateDir); + if (access(stateDir.c_str(), R_OK | W_OK) == 0) + return make_ref(params); + else if (pathExists(settings.nixDaemonSocketFile)) + return make_ref(params); +#ifdef __linux__ + else if ( + !pathExists(stateDir) && params.empty() && !isRootUser() && !getEnv("NIX_STORE_DIR").has_value() + && !getEnv("NIX_STATE_DIR").has_value()) { + /* If /nix doesn't exist, there is no daemon socket, and + we're not root, then automatically set up a chroot + store in ~/.local/share/nix/root. */ + auto chrootStore = getDataDir() + "/root"; + if (!pathExists(chrootStore)) { + try { + createDirs(chrootStore); + } catch (SystemError & e) { + return make_ref(params); + } + warn("'%s' does not exist, so Nix will use '%s' as a chroot store", stateDir, chrootStore); + } else + debug("'%s' does not exist, so Nix will use '%s' as a chroot store", stateDir, chrootStore); + return make_ref("local", chrootStore, params); + } +#endif + else + return make_ref(params); + }, + [&](const StoreReference::Specified & g) { + for (const auto & [storeName, implem] : Implementations::registered()) + if (implem.uriSchemes.count(g.scheme)) + return implem.parseConfig(g.scheme, g.authority, params); + + throw Error("don't know how to open Nix store with scheme '%s'", g.scheme); + }, + }, + storeURI.variant); + + experimentalFeatureSettings.require(storeConfig->experimentalFeature()); + storeConfig->warnUnknownSettings(); + + return storeConfig; +} + +std::list> getDefaultSubstituters() +{ + static auto stores([]() { + std::list> stores; + + StringSet done; + + auto addStore = [&](const std::string & uri) { + if (!done.insert(uri).second) + return; + try { + stores.push_back(openStore(uri)); + } catch (Error & e) { + logWarning(e.info()); + } + }; + + for (const auto & uri : settings.substituters.get()) + addStore(uri); + + stores.sort([](ref & a, ref & b) { return a->config.priority < b->config.priority; }); + + return stores; + }()); + + return stores; +} + +Implementations::Map & Implementations::registered() +{ + static Map registered; + return registered; +} + +} diff --git a/src/libstore/uds-remote-store.cc b/src/libstore/uds-remote-store.cc index 3c445eb1318..c979b5e47c5 100644 --- a/src/libstore/uds-remote-store.cc +++ b/src/libstore/uds-remote-store.cc @@ -1,6 +1,7 @@ -#include "uds-remote-store.hh" -#include "unix-domain-socket.hh" -#include "worker-protocol.hh" +#include "nix/store/uds-remote-store.hh" +#include "nix/util/unix-domain-socket.hh" +#include "nix/store/worker-protocol.hh" +#include "nix/store/store-registration.hh" #include #include @@ -20,13 +21,13 @@ namespace nix { UDSRemoteStoreConfig::UDSRemoteStoreConfig( std::string_view scheme, std::string_view authority, - const Params & params) - : StoreConfig(params) - , LocalFSStoreConfig(params) - , RemoteStoreConfig(params) + const StoreReference::Params & params) + : Store::Config{params} + , LocalFSStore::Config{params} + , RemoteStore::Config{params} , path{authority.empty() ? settings.nixDaemonSocketFile : authority} { - if (scheme != UDSRemoteStoreConfig::scheme) { + if (uriSchemes().count(scheme) == 0) { throw UsageError("Scheme must be 'unix'"); } } @@ -44,32 +45,30 @@ std::string UDSRemoteStoreConfig::doc() // empty string will later default to the same nixDaemonSocketFile. Why // don't we just wire it all through? I believe there are cases where it // will live reload so we want to continue to account for that. -UDSRemoteStore::UDSRemoteStore(const Params & params) - : UDSRemoteStore(scheme, "", params) -{} - - -UDSRemoteStore::UDSRemoteStore(std::string_view scheme, std::string_view authority, const Params & params) - : StoreConfig(params) - , LocalFSStoreConfig(params) - , RemoteStoreConfig(params) - , UDSRemoteStoreConfig(scheme, authority, params) - , Store(params) - , LocalFSStore(params) - , RemoteStore(params) +UDSRemoteStoreConfig::UDSRemoteStoreConfig(const Params & params) + : UDSRemoteStoreConfig(*uriSchemes().begin(), "", params) +{ +} + + +UDSRemoteStore::UDSRemoteStore(ref config) + : Store{*config} + , LocalFSStore{*config} + , RemoteStore{*config} + , config{config} { } std::string UDSRemoteStore::getUri() { - return path == settings.nixDaemonSocketFile + return config->path == settings.nixDaemonSocketFile ? // FIXME: Not clear why we return daemon here and not default // to settings.nixDaemonSocketFile // // unix:// with no path also works. Change what we return? "daemon" - : std::string(scheme) + "://" + path; + : std::string(*Config::uriSchemes().begin()) + "://" + config->path; } @@ -84,9 +83,7 @@ ref UDSRemoteStore::openConnection() auto conn = make_ref(); /* Connect to a daemon that does the privileged work for us. */ - conn->fd = createUnixDomainSocket(); - - nix::connect(toSocket(conn->fd.get()), path); + conn->fd = nix::connect(config->path); conn->from.fd = conn->fd.get(); conn->to.fd = conn->fd.get(); @@ -106,6 +103,11 @@ void UDSRemoteStore::addIndirectRoot(const Path & path) } -static RegisterStoreImplementation regUDSRemoteStore; +ref UDSRemoteStore::Config::openStore() const { + return make_ref(ref{shared_from_this()}); +} + + +static RegisterStoreImplementation regUDSRemoteStore; } diff --git a/src/libstore/unix/build/child.cc b/src/libstore/unix/build/child.cc index aa31c3caf24..a21fddf5176 100644 --- a/src/libstore/unix/build/child.cc +++ b/src/libstore/unix/build/child.cc @@ -1,6 +1,6 @@ -#include "child.hh" -#include "current-process.hh" -#include "logging.hh" +#include "nix/store/build/child.hh" +#include "nix/util/current-process.hh" +#include "nix/util/logging.hh" #include #include diff --git a/src/libstore/unix/build/darwin-derivation-builder.cc b/src/libstore/unix/build/darwin-derivation-builder.cc new file mode 100644 index 00000000000..3985498c1c4 --- /dev/null +++ b/src/libstore/unix/build/darwin-derivation-builder.cc @@ -0,0 +1,211 @@ +#ifdef __APPLE__ + +# include +# include +# include + +/* This definition is undocumented but depended upon by all major browsers. */ +extern "C" int +sandbox_init_with_parameters(const char * profile, uint64_t flags, const char * const parameters[], char ** errorbuf); + +namespace nix { + +struct DarwinDerivationBuilder : DerivationBuilderImpl +{ + PathsInChroot pathsInChroot; + + /** + * Whether full sandboxing is enabled. Note that macOS builds + * always have *some* sandboxing (see sandbox-minimal.sb). + */ + bool useSandbox; + + DarwinDerivationBuilder( + Store & store, + std::unique_ptr miscMethods, + DerivationBuilderParams params, + bool useSandbox) + : DerivationBuilderImpl(store, std::move(miscMethods), std::move(params)) + , useSandbox(useSandbox) + { + } + + void prepareSandbox() override + { + pathsInChroot = getPathsInSandbox(); + } + + void setUser() override + { + DerivationBuilderImpl::setUser(); + + /* This has to appear before import statements. */ + std::string sandboxProfile = "(version 1)\n"; + + if (useSandbox) { + + /* Lots and lots and lots of file functions freak out if they can't stat their full ancestry */ + PathSet ancestry; + + /* We build the ancestry before adding all inputPaths to the store because we know they'll + all have the same parents (the store), and there might be lots of inputs. This isn't + particularly efficient... I doubt it'll be a bottleneck in practice */ + for (auto & i : pathsInChroot) { + Path cur = i.first; + while (cur.compare("/") != 0) { + cur = dirOf(cur); + ancestry.insert(cur); + } + } + + /* And we want the store in there regardless of how empty pathsInChroot. We include the innermost + path component this time, since it's typically /nix/store and we care about that. */ + Path cur = store.storeDir; + while (cur.compare("/") != 0) { + ancestry.insert(cur); + cur = dirOf(cur); + } + + /* Add all our input paths to the chroot */ + for (auto & i : inputPaths) { + auto p = store.printStorePath(i); + pathsInChroot.insert_or_assign(p, p); + } + + /* Violations will go to the syslog if you set this. Unfortunately the destination does not appear to be + * configurable */ + if (settings.darwinLogSandboxViolations) { + sandboxProfile += "(deny default)\n"; + } else { + sandboxProfile += "(deny default (with no-log))\n"; + } + + sandboxProfile += +# include "sandbox-defaults.sb" + ; + + if (!derivationType.isSandboxed()) + sandboxProfile += +# include "sandbox-network.sb" + ; + + /* Add the output paths we'll use at build-time to the chroot */ + sandboxProfile += "(allow file-read* file-write* process-exec\n"; + for (auto & [_, path] : scratchOutputs) + sandboxProfile += fmt("\t(subpath \"%s\")\n", store.printStorePath(path)); + + sandboxProfile += ")\n"; + + /* Our inputs (transitive dependencies and any impurities computed above) + + without file-write* allowed, access() incorrectly returns EPERM + */ + sandboxProfile += "(allow file-read* file-write* process-exec\n"; + + // We create multiple allow lists, to avoid exceeding a limit in the darwin sandbox interpreter. + // See https://github.com/NixOS/nix/issues/4119 + // We split our allow groups approximately at half the actual limit, 1 << 16 + const size_t breakpoint = sandboxProfile.length() + (1 << 14); + for (auto & i : pathsInChroot) { + + if (sandboxProfile.length() >= breakpoint) { + debug("Sandbox break: %d %d", sandboxProfile.length(), breakpoint); + sandboxProfile += ")\n(allow file-read* file-write* process-exec\n"; + } + + if (i.first != i.second.source) + throw Error( + "can't map '%1%' to '%2%': mismatched impure paths not supported on Darwin", + i.first, + i.second.source); + + std::string path = i.first; + auto optSt = maybeLstat(path.c_str()); + if (!optSt) { + if (i.second.optional) + continue; + throw SysError("getting attributes of required path '%s", path); + } + if (S_ISDIR(optSt->st_mode)) + sandboxProfile += fmt("\t(subpath \"%s\")\n", path); + else + sandboxProfile += fmt("\t(literal \"%s\")\n", path); + } + sandboxProfile += ")\n"; + + /* Allow file-read* on full directory hierarchy to self. Allows realpath() */ + sandboxProfile += "(allow file-read*\n"; + for (auto & i : ancestry) { + sandboxProfile += fmt("\t(literal \"%s\")\n", i); + } + sandboxProfile += ")\n"; + + sandboxProfile += drvOptions.additionalSandboxProfile; + } else + sandboxProfile += +# include "sandbox-minimal.sb" + ; + + debug("Generated sandbox profile:"); + debug(sandboxProfile); + + /* The tmpDir in scope points at the temporary build directory for our derivation. Some packages try different + mechanisms to find temporary directories, so we want to open up a broader place for them to put their files, + if needed. */ + Path globalTmpDir = canonPath(defaultTempDir(), true); + + /* They don't like trailing slashes on subpath directives */ + while (!globalTmpDir.empty() && globalTmpDir.back() == '/') + globalTmpDir.pop_back(); + + if (getEnv("_NIX_TEST_NO_SANDBOX") != "1") { + Strings sandboxArgs; + sandboxArgs.push_back("_NIX_BUILD_TOP"); + sandboxArgs.push_back(tmpDir); + sandboxArgs.push_back("_GLOBAL_TMP_DIR"); + sandboxArgs.push_back(globalTmpDir); + if (drvOptions.allowLocalNetworking) { + sandboxArgs.push_back("_ALLOW_LOCAL_NETWORKING"); + sandboxArgs.push_back("1"); + } + char * sandbox_errbuf = nullptr; + if (sandbox_init_with_parameters( + sandboxProfile.c_str(), 0, stringsToCharPtrs(sandboxArgs).data(), &sandbox_errbuf)) { + writeFull( + STDERR_FILENO, + fmt("failed to configure sandbox: %s\n", sandbox_errbuf ? sandbox_errbuf : "(null)")); + _exit(1); + } + } + } + + void execBuilder(const Strings & args, const Strings & envStrs) override + { + posix_spawnattr_t attrp; + + if (posix_spawnattr_init(&attrp)) + throw SysError("failed to initialize builder"); + + if (posix_spawnattr_setflags(&attrp, POSIX_SPAWN_SETEXEC)) + throw SysError("failed to initialize builder"); + + if (drv.platform == "aarch64-darwin") { + // Unset kern.curproc_arch_affinity so we can escape Rosetta + int affinity = 0; + sysctlbyname("kern.curproc_arch_affinity", NULL, NULL, &affinity, sizeof(affinity)); + + cpu_type_t cpu = CPU_TYPE_ARM64; + posix_spawnattr_setbinpref_np(&attrp, 1, &cpu, NULL); + } else if (drv.platform == "x86_64-darwin") { + cpu_type_t cpu = CPU_TYPE_X86_64; + posix_spawnattr_setbinpref_np(&attrp, 1, &cpu, NULL); + } + + posix_spawn( + NULL, drv.builder.c_str(), NULL, &attrp, stringsToCharPtrs(args).data(), stringsToCharPtrs(envStrs).data()); + } +}; + +} + +#endif diff --git a/src/libstore/unix/build/derivation-builder.cc b/src/libstore/unix/build/derivation-builder.cc new file mode 100644 index 00000000000..cf6c0a5b134 --- /dev/null +++ b/src/libstore/unix/build/derivation-builder.cc @@ -0,0 +1,2258 @@ +#include "nix/store/build/derivation-builder.hh" +#include "nix/util/file-system.hh" +#include "nix/store/local-store.hh" +#include "nix/util/processes.hh" +#include "nix/store/builtins.hh" +#include "nix/store/path-references.hh" +#include "nix/util/finally.hh" +#include "nix/util/util.hh" +#include "nix/util/archive.hh" +#include "nix/util/git.hh" +#include "nix/store/daemon.hh" +#include "nix/util/topo-sort.hh" +#include "nix/store/build/child.hh" +#include "nix/util/unix-domain-socket.hh" +#include "nix/store/posix-fs-canonicalise.hh" +#include "nix/util/posix-source-accessor.hh" +#include "nix/store/restricted-store.hh" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "store-config-private.hh" + +#if HAVE_STATVFS +# include +#endif + +#include +#include +#include + +#include "nix/util/strings.hh" +#include "nix/util/signals.hh" + +#include "store-config-private.hh" + +namespace nix { + +MakeError(NotDeterministic, BuildError); + +/** + * This class represents the state for building locally. + * + * @todo Ideally, it would not be a class, but a single function. + * However, besides the main entry point, there are a few more methods + * which are externally called, and need to be gotten rid of. There are + * also some virtual methods (either directly here or inherited from + * `DerivationBuilderCallbacks`, a stop-gap) that represent outgoing + * rather than incoming call edges that either should be removed, or + * become (higher order) function parameters. + */ +// FIXME: rename this to UnixDerivationBuilder or something like that. +class DerivationBuilderImpl : public DerivationBuilder, public DerivationBuilderParams +{ +protected: + + Store & store; + + std::unique_ptr miscMethods; + +public: + + DerivationBuilderImpl( + Store & store, + std::unique_ptr miscMethods, + DerivationBuilderParams params) + : DerivationBuilderParams{std::move(params)} + , store{store} + , miscMethods{std::move(miscMethods)} + , derivationType{drv.type()} + { } + +protected: + + /** + * User selected for running the builder. + */ + std::unique_ptr buildUser; + + /** + * The temporary directory used for the build. + */ + Path tmpDir; + + /** + * The top-level temporary directory. `tmpDir` is either equal to + * or a child of this directory. + */ + Path topTmpDir; + + /** + * The file descriptor of the temporary directory. + */ + AutoCloseFD tmpDirFd; + + /** + * The sort of derivation we are building. + * + * Just a cached value, computed from `drv`. + */ + const DerivationType derivationType; + + /** + * Stuff we need to pass to initChild(). + */ + struct ChrootPath { + Path source; + bool optional; + ChrootPath(Path source = "", bool optional = false) + : source(source), optional(optional) + { } + }; + typedef std::map PathsInChroot; // maps target path to source path + + typedef StringMap Environment; + Environment env; + + /** + * Hash rewriting. + */ + StringMap inputRewrites, outputRewrites; + typedef std::map RedirectedOutputs; + RedirectedOutputs redirectedOutputs; + + /** + * The output paths used during the build. + * + * - Input-addressed derivations or fixed content-addressed outputs are + * sometimes built when some of their outputs already exist, and can not + * be hidden via sandboxing. We use temporary locations instead and + * rewrite after the build. Otherwise the regular predetermined paths are + * put here. + * + * - Floating content-addressing derivations do not know their final build + * output paths until the outputs are hashed, so random locations are + * used, and then renamed. The randomness helps guard against hidden + * self-references. + */ + OutputPathMap scratchOutputs; + + const static Path homeDir; + + /** + * The recursive Nix daemon socket. + */ + AutoCloseFD daemonSocket; + + /** + * The daemon main thread. + */ + std::thread daemonThread; + + /** + * The daemon worker threads. + */ + std::vector daemonWorkerThreads; + + const StorePathSet & originalPaths() override + { + return inputPaths; + } + + bool isAllowed(const StorePath & path) override + { + return inputPaths.count(path) || addedPaths.count(path); + } + bool isAllowed(const DrvOutput & id) override + { + return addedDrvOutputs.count(id); + } + + bool isAllowed(const DerivedPath & req); + + friend struct RestrictedStore; + + /** + * Whether we need to perform hash rewriting if there are valid output paths. + */ + virtual bool needsHashRewrite() + { + return true; + } + +public: + + bool prepareBuild() override; + + void startBuilder() override; + + std::variant, SingleDrvOutputs> unprepareBuild() override; + +protected: + + /** + * Acquire a build user lock. Return nullptr if no lock is available. + */ + virtual std::unique_ptr getBuildUser() + { + return acquireUserLock(1, false); + } + + /** + * Return the paths that should be made available in the sandbox. + * This includes: + * + * * The paths specified by the `sandbox-paths` setting, and their closure in the Nix store. + * * The contents of the `__impureHostDeps` derivation attribute, if the sandbox is in relaxed mode. + * * The paths returned by the `pre-build-hook`. + * * The paths in the input closure of the derivation. + */ + PathsInChroot getPathsInSandbox(); + + virtual void setBuildTmpDir() + { + tmpDir = topTmpDir; + } + + /** + * Return the path of the temporary directory in the sandbox. + */ + virtual Path tmpDirInSandbox() + { + assert(!topTmpDir.empty()); + return topTmpDir; + } + + /** + * Ensure that there are no processes running that conflict with + * `buildUser`. + */ + virtual void prepareUser() + { + killSandbox(false); + } + + /** + * Called by prepareBuild() to do any setup in the parent to + * prepare for a sandboxed build. + */ + virtual void prepareSandbox(); + + virtual Strings getPreBuildHookArgs() + { + return Strings({store.printStorePath(drvPath)}); + } + + virtual Path realPathInSandbox(const Path & p) + { + return store.toRealPath(p); + } + + /** + * Open the slave side of the pseudoterminal and use it as stderr. + */ + void openSlave(); + + /** + * Called by prepareBuild() to start the child process for the + * build. Must set `pid`. The child must call runChild(). + */ + virtual void startChild(); + +private: + + /** + * Fill in the environment for the builder. + */ + void initEnv(); + +protected: + + /** + * Process messages send by the sandbox initialization. + */ + void processSandboxSetupMessages(); + +private: + + /** + * Write a JSON file containing the derivation attributes. + */ + void writeStructuredAttrs(); + + /** + * Start an in-process nix daemon thread for recursive-nix. + */ + void startDaemon(); + +public: + + void stopDaemon() override; + +private: + + void addDependency(const StorePath & path) override; + +protected: + + /** + * Make a file owned by the builder. + * + * SAFETY: this function is prone to TOCTOU as it receives a path and not a descriptor. + * It's only safe to call in a child of a directory only visible to the owner. + */ + void chownToBuilder(const Path & path); + + /** + * Make a file owned by the builder addressed by its file descriptor. + */ + void chownToBuilder(int fd, const Path & path); + + /** + * Create a file in `tmpDir` owned by the builder. + */ + void writeBuilderFile( + const std::string & name, + std::string_view contents); + + /** + * Run the builder's process. + */ + void runChild(); + + /** + * Move the current process into the chroot, if any. Called early + * by runChild(). + */ + virtual void enterChroot() + { + } + + /** + * Change the current process's uid/gid to the build user, if + * any. Called by runChild(). + */ + virtual void setUser(); + + /** + * Execute the derivation builder process. Called by runChild() as + * its final step. Should not return unless there is an error. + */ + virtual void execBuilder(const Strings & args, const Strings & envStrs); + +private: + + /** + * Check that the derivation outputs all exist and register them + * as valid. + */ + SingleDrvOutputs registerOutputs(); + + /** + * Check that an output meets the requirements specified by the + * 'outputChecks' attribute (or the legacy + * '{allowed,disallowed}{References,Requisites}' attributes). + */ + void checkOutputs(const std::map & outputs); + +public: + + void deleteTmpDir(bool force) override; + + void killSandbox(bool getStats) override; + +protected: + + virtual void cleanupBuild(); + +private: + + bool decideWhetherDiskFull(); + + /** + * Create alternative path calculated from but distinct from the + * input, so we can avoid overwriting outputs (or other store paths) + * that already exist. + */ + StorePath makeFallbackPath(const StorePath & path); + + /** + * Make a path to another based on the output name along with the + * derivation hash. + * + * @todo Add option to randomize, so we can audit whether our + * rewrites caught everything + */ + StorePath makeFallbackPath(OutputNameView outputName); +}; + +void handleDiffHook( + uid_t uid, uid_t gid, + const Path & tryA, const Path & tryB, + const Path & drvPath, const Path & tmpDir) +{ + auto & diffHookOpt = settings.diffHook.get(); + if (diffHookOpt && settings.runDiffHook) { + auto & diffHook = *diffHookOpt; + try { + auto diffRes = runProgram(RunOptions { + .program = diffHook, + .lookupPath = true, + .args = {tryA, tryB, drvPath, tmpDir}, + .uid = uid, + .gid = gid, + .chdir = "/" + }); + if (!statusOk(diffRes.first)) + throw ExecError(diffRes.first, + "diff-hook program '%1%' %2%", + diffHook, + statusToString(diffRes.first)); + + if (diffRes.second != "") + printError(chomp(diffRes.second)); + } catch (Error & error) { + ErrorInfo ei = error.info(); + // FIXME: wrap errors. + ei.msg = HintFmt("diff hook execution failed: %s", ei.msg.str()); + logError(ei); + } + } +} + +const Path DerivationBuilderImpl::homeDir = "/homeless-shelter"; + + +static LocalStore & getLocalStore(Store & store) +{ + auto p = dynamic_cast(&store); + assert(p); + return *p; +} + + +void DerivationBuilderImpl::killSandbox(bool getStats) +{ + if (buildUser) { + auto uid = buildUser->getUID(); + assert(uid != 0); + killUser(uid); + } +} + + +bool DerivationBuilderImpl::prepareBuild() +{ + if (useBuildUsers()) { + if (!buildUser) + buildUser = getBuildUser(); + + if (!buildUser) + return false; + } + + return true; +} + + +std::variant, SingleDrvOutputs> DerivationBuilderImpl::unprepareBuild() +{ + // FIXME: get rid of this, rely on RAII. + Finally releaseBuildUser([&](){ + /* Release the build user at the end of this function. We don't do + it right away because we don't want another build grabbing this + uid and then messing around with our output. */ + buildUser.reset(); + }); + + /* Since we got an EOF on the logger pipe, the builder is presumed + to have terminated. In fact, the builder could also have + simply have closed its end of the pipe, so just to be sure, + kill it. */ + int status = pid.kill(); + + debug("builder process for '%s' finished", store.printStorePath(drvPath)); + + buildResult.timesBuilt++; + buildResult.stopTime = time(0); + + /* So the child is gone now. */ + miscMethods->childTerminated(); + + /* Close the read side of the logger pipe. */ + builderOut.close(); + + /* Close the log file. */ + miscMethods->closeLogFile(); + + /* When running under a build user, make sure that all processes + running under that uid are gone. This is to prevent a + malicious user from leaving behind a process that keeps files + open and modifies them after they have been chown'ed to + root. */ + killSandbox(true); + + /* Terminate the recursive Nix daemon. */ + stopDaemon(); + + if (buildResult.cpuUser && buildResult.cpuSystem) { + debug("builder for '%s' terminated with status %d, user CPU %.3fs, system CPU %.3fs", + store.printStorePath(drvPath), + status, + ((double) buildResult.cpuUser->count()) / 1000000, + ((double) buildResult.cpuSystem->count()) / 1000000); + } + + bool diskFull = false; + + try { + + /* Check the exit status. */ + if (!statusOk(status)) { + + diskFull |= decideWhetherDiskFull(); + + cleanupBuild(); + + auto msg = fmt( + "Cannot build '%s'.\n" + "Reason: " ANSI_RED "builder %s" ANSI_NORMAL ".", + Magenta(store.printStorePath(drvPath)), + statusToString(status)); + + msg += showKnownOutputs(store, drv); + + miscMethods->appendLogTailErrorMsg(msg); + + if (diskFull) + msg += "\nnote: build failure may have been caused by lack of free disk space"; + + throw BuildError(msg); + } + + /* Compute the FS closure of the outputs and register them as + being valid. */ + auto builtOutputs = registerOutputs(); + + StorePathSet outputPaths; + for (auto & [_, output] : builtOutputs) + outputPaths.insert(output.outPath); + runPostBuildHook( + store, + *logger, + drvPath, + outputPaths + ); + + /* Delete unused redirected outputs (when doing hash rewriting). */ + for (auto & i : redirectedOutputs) + deletePath(store.Store::toRealPath(i.second)); + + deleteTmpDir(true); + + return std::move(builtOutputs); + + } catch (BuildError & e) { + BuildResult::Status st = + dynamic_cast(&e) ? BuildResult::NotDeterministic : + statusOk(status) ? BuildResult::OutputRejected : + !derivationType.isSandboxed() || diskFull ? BuildResult::TransientFailure : + BuildResult::PermanentFailure; + + return std::pair{std::move(st), std::move(e)}; + } +} + +void DerivationBuilderImpl::cleanupBuild() +{ + deleteTmpDir(false); +} + +static void chmod_(const Path & path, mode_t mode) +{ + if (chmod(path.c_str(), mode) == -1) + throw SysError("setting permissions on '%s'", path); +} + + +/* Move/rename path 'src' to 'dst'. Temporarily make 'src' writable if + it's a directory and we're not root (to be able to update the + directory's parent link ".."). */ +static void movePath(const Path & src, const Path & dst) +{ + auto st = lstat(src); + + bool changePerm = (geteuid() && S_ISDIR(st.st_mode) && !(st.st_mode & S_IWUSR)); + + if (changePerm) + chmod_(src, st.st_mode | S_IWUSR); + + std::filesystem::rename(src, dst); + + if (changePerm) + chmod_(dst, st.st_mode); +} + + +static void replaceValidPath(const Path & storePath, const Path & tmpPath) +{ + /* We can't atomically replace storePath (the original) with + tmpPath (the replacement), so we have to move it out of the + way first. We'd better not be interrupted here, because if + we're repairing (say) Glibc, we end up with a broken system. */ + Path oldPath; + + if (pathExists(storePath)) { + // why do we loop here? + // although makeTempPath should be unique, we can't + // guarantee that. + do { + oldPath = makeTempPath(storePath, ".old"); + // store paths are often directories so we can't just unlink() it + // let's make sure the path doesn't exist before we try to use it + } while (pathExists(oldPath)); + movePath(storePath, oldPath); + } + try { + movePath(tmpPath, storePath); + } catch (...) { + try { + // attempt to recover + if (!oldPath.empty()) + movePath(oldPath, storePath); + } catch (...) { + ignoreExceptionExceptInterrupt(); + } + throw; + } + if (!oldPath.empty()) + deletePath(oldPath); +} + +bool DerivationBuilderImpl::decideWhetherDiskFull() +{ + bool diskFull = false; + + /* Heuristically check whether the build failure may have + been caused by a disk full condition. We have no way + of knowing whether the build actually got an ENOSPC. + So instead, check if the disk is (nearly) full now. If + so, we don't mark this build as a permanent failure. */ +#if HAVE_STATVFS + { + auto & localStore = getLocalStore(store); + uint64_t required = 8ULL * 1024 * 1024; // FIXME: make configurable + struct statvfs st; + if (statvfs(localStore.config->realStoreDir.get().c_str(), &st) == 0 && + (uint64_t) st.f_bavail * st.f_bsize < required) + diskFull = true; + if (statvfs(tmpDir.c_str(), &st) == 0 && + (uint64_t) st.f_bavail * st.f_bsize < required) + diskFull = true; + } +#endif + + return diskFull; +} + +/** + * Rethrow the current exception as a subclass of `Error`. + */ +static void rethrowExceptionAsError() +{ + try { + throw; + } catch (Error &) { + throw; + } catch (std::exception & e) { + throw Error(e.what()); + } catch (...) { + throw Error("unknown exception"); + } +} + +/** + * Send the current exception to the parent in the format expected by + * `DerivationBuilderImpl::processSandboxSetupMessages()`. + */ +static void handleChildException(bool sendException) +{ + try { + rethrowExceptionAsError(); + } catch (Error & e) { + if (sendException) { + writeFull(STDERR_FILENO, "\1\n"); + FdSink sink(STDERR_FILENO); + sink << e; + sink.flush(); + } else + std::cerr << e.msg(); + } +} + +static bool checkNotWorldWritable(std::filesystem::path path) +{ + while (true) { + auto st = lstat(path); + if (st.st_mode & S_IWOTH) + return false; + if (path == path.parent_path()) break; + path = path.parent_path(); + } + return true; +} + +void DerivationBuilderImpl::startBuilder() +{ + /* Make sure that no other processes are executing under the + sandbox uids. This must be done before any chownToBuilder() + calls. */ + prepareUser(); + + /* Right platform? */ + if (!drvOptions.canBuildLocally(store, drv)) { + auto msg = fmt( + "Cannot build '%s'.\n" + "Reason: " ANSI_RED "required system or feature not available" ANSI_NORMAL "\n" + "Required system: '%s' with features {%s}\n" + "Current system: '%s' with features {%s}", + Magenta(store.printStorePath(drvPath)), + Magenta(drv.platform), + concatStringsSep(", ", drvOptions.getRequiredSystemFeatures(drv)), + Magenta(settings.thisSystem), + concatStringsSep(", ", store.config.systemFeatures)); + + // since aarch64-darwin has Rosetta 2, this user can actually run x86_64-darwin on their hardware - we should tell them to run the command to install Darwin 2 + if (drv.platform == "x86_64-darwin" && settings.thisSystem == "aarch64-darwin") + msg += fmt("\nNote: run `%s` to run programs for x86_64-darwin", Magenta("/usr/sbin/softwareupdate --install-rosetta && launchctl stop org.nixos.nix-daemon")); + + throw BuildError(msg); + } + + auto buildDir = getLocalStore(store).config->getBuildDir(); + + createDirs(buildDir); + + if (buildUser && !checkNotWorldWritable(buildDir)) + throw Error("Path %s or a parent directory is world-writable or a symlink. That's not allowed for security.", buildDir); + + /* Create a temporary directory where the build will take + place. */ + topTmpDir = createTempDir(buildDir, "nix-build-" + std::string(drvPath.name()), 0700); + setBuildTmpDir(); + assert(!tmpDir.empty()); + + /* The TOCTOU between the previous mkdir call and this open call is unavoidable due to + POSIX semantics.*/ + tmpDirFd = AutoCloseFD{open(tmpDir.c_str(), O_RDONLY | O_NOFOLLOW | O_DIRECTORY)}; + if (!tmpDirFd) + throw SysError("failed to open the build temporary directory descriptor '%1%'", tmpDir); + + chownToBuilder(tmpDirFd.get(), tmpDir); + + for (auto & [outputName, status] : initialOutputs) { + /* Set scratch path we'll actually use during the build. + + If we're not doing a chroot build, but we have some valid + output paths. Since we can't just overwrite or delete + them, we have to do hash rewriting: i.e. in the + environment/arguments passed to the build, we replace the + hashes of the valid outputs with unique dummy strings; + after the build, we discard the redirected outputs + corresponding to the valid outputs, and rewrite the + contents of the new outputs to replace the dummy strings + with the actual hashes. */ + auto scratchPath = + !status.known + ? makeFallbackPath(outputName) + : !needsHashRewrite() + /* Can always use original path in sandbox */ + ? status.known->path + : !status.known->isPresent() + /* If path doesn't yet exist can just use it */ + ? status.known->path + : buildMode != bmRepair && !status.known->isValid() + /* If we aren't repairing we'll delete a corrupted path, so we + can use original path */ + ? status.known->path + : /* If we are repairing or the path is totally valid, we'll need + to use a temporary path */ + makeFallbackPath(status.known->path); + scratchOutputs.insert_or_assign(outputName, scratchPath); + + /* Substitute output placeholders with the scratch output paths. + We'll use during the build. */ + inputRewrites[hashPlaceholder(outputName)] = store.printStorePath(scratchPath); + + /* Additional tasks if we know the final path a priori. */ + if (!status.known) continue; + auto fixedFinalPath = status.known->path; + + /* Additional tasks if the final and scratch are both known and + differ. */ + if (fixedFinalPath == scratchPath) continue; + + /* Ensure scratch path is ours to use. */ + deletePath(store.printStorePath(scratchPath)); + + /* Rewrite and unrewrite paths */ + { + std::string h1 { fixedFinalPath.hashPart() }; + std::string h2 { scratchPath.hashPart() }; + inputRewrites[h1] = h2; + } + + redirectedOutputs.insert_or_assign(std::move(fixedFinalPath), std::move(scratchPath)); + } + + /* Construct the environment passed to the builder. */ + initEnv(); + + writeStructuredAttrs(); + + /* Handle exportReferencesGraph(), if set. */ + if (!parsedDrv) { + for (auto & [fileName, ss] : drvOptions.exportReferencesGraph) { + StorePathSet storePathSet; + for (auto & storePathS : ss) { + if (!store.isInStore(storePathS)) + throw BuildError("'exportReferencesGraph' contains a non-store path '%1%'", storePathS); + storePathSet.insert(store.toStorePath(storePathS).first); + } + /* Write closure info to . */ + writeFile(tmpDir + "/" + fileName, + store.makeValidityRegistration( + store.exportReferences(storePathSet, inputPaths), false, false)); + } + } + + prepareSandbox(); + + if (needsHashRewrite() && pathExists(homeDir)) + throw Error("home directory '%1%' exists; please remove it to assure purity of builds without sandboxing", homeDir); + + /* Fire up a Nix daemon to process recursive Nix calls from the + builder. */ + if (drvOptions.getRequiredSystemFeatures(drv).count("recursive-nix")) + startDaemon(); + + /* Run the builder. */ + printMsg(lvlChatty, "executing builder '%1%'", drv.builder); + printMsg(lvlChatty, "using builder args '%1%'", concatStringsSep(" ", drv.args)); + for (auto & i : drv.env) + printMsg(lvlVomit, "setting builder env variable '%1%'='%2%'", i.first, i.second); + + /* Create the log file. */ + miscMethods->openLogFile(); + + /* Create a pseudoterminal to get the output of the builder. */ + builderOut = posix_openpt(O_RDWR | O_NOCTTY); + if (!builderOut) + throw SysError("opening pseudoterminal master"); + + // FIXME: not thread-safe, use ptsname_r + std::string slaveName = ptsname(builderOut.get()); + + if (buildUser) { + if (chmod(slaveName.c_str(), 0600)) + throw SysError("changing mode of pseudoterminal slave"); + + if (chown(slaveName.c_str(), buildUser->getUID(), 0)) + throw SysError("changing owner of pseudoterminal slave"); + } +#ifdef __APPLE__ + else { + if (grantpt(builderOut.get())) + throw SysError("granting access to pseudoterminal slave"); + } +#endif + + if (unlockpt(builderOut.get())) + throw SysError("unlocking pseudoterminal"); + + buildResult.startTime = time(0); + + /* Start a child process to build the derivation. */ + startChild(); + + pid.setSeparatePG(true); + miscMethods->childStarted(builderOut.get()); + + processSandboxSetupMessages(); +} + +DerivationBuilderImpl::PathsInChroot DerivationBuilderImpl::getPathsInSandbox() +{ + PathsInChroot pathsInChroot; + + /* Allow a user-configurable set of directories from the + host file system. */ + for (auto i : settings.sandboxPaths.get()) { + if (i.empty()) continue; + bool optional = false; + if (i[i.size() - 1] == '?') { + optional = true; + i.pop_back(); + } + size_t p = i.find('='); + if (p == std::string::npos) + pathsInChroot[i] = {i, optional}; + else + pathsInChroot[i.substr(0, p)] = {i.substr(p + 1), optional}; + } + if (hasPrefix(store.storeDir, tmpDirInSandbox())) + { + throw Error("`sandbox-build-dir` must not contain the storeDir"); + } + pathsInChroot[tmpDirInSandbox()] = tmpDir; + + /* Add the closure of store paths to the chroot. */ + StorePathSet closure; + for (auto & i : pathsInChroot) + try { + if (store.isInStore(i.second.source)) + store.computeFSClosure(store.toStorePath(i.second.source).first, closure); + } catch (InvalidPath & e) { + } catch (Error & e) { + e.addTrace({}, "while processing sandbox path '%s'", i.second.source); + throw; + } + for (auto & i : closure) { + auto p = store.printStorePath(i); + pathsInChroot.insert_or_assign(p, p); + } + + PathSet allowedPaths = settings.allowedImpureHostPrefixes; + + /* This works like the above, except on a per-derivation level */ + auto impurePaths = drvOptions.impureHostDeps; + + for (auto & i : impurePaths) { + bool found = false; + /* Note: we're not resolving symlinks here to prevent + giving a non-root user info about inaccessible + files. */ + Path canonI = canonPath(i); + /* If only we had a trie to do this more efficiently :) luckily, these are generally going to be pretty small */ + for (auto & a : allowedPaths) { + Path canonA = canonPath(a); + if (isDirOrInDir(canonI, canonA)) { + found = true; + break; + } + } + if (!found) + throw Error("derivation '%s' requested impure path '%s', but it was not in allowed-impure-host-deps", + store.printStorePath(drvPath), i); + + /* Allow files in drvOptions.impureHostDeps to be missing; e.g. + macOS 11+ has no /usr/lib/libSystem*.dylib */ + pathsInChroot[i] = {i, true}; + } + + if (settings.preBuildHook != "") { + printMsg(lvlChatty, "executing pre-build hook '%1%'", settings.preBuildHook); + enum BuildHookState { + stBegin, + stExtraChrootDirs + }; + auto state = stBegin; + auto lines = runProgram(settings.preBuildHook, false, getPreBuildHookArgs()); + auto lastPos = std::string::size_type{0}; + for (auto nlPos = lines.find('\n'); nlPos != std::string::npos; + nlPos = lines.find('\n', lastPos)) + { + auto line = lines.substr(lastPos, nlPos - lastPos); + lastPos = nlPos + 1; + if (state == stBegin) { + if (line == "extra-sandbox-paths" || line == "extra-chroot-dirs") { + state = stExtraChrootDirs; + } else { + throw Error("unknown pre-build hook command '%1%'", line); + } + } else if (state == stExtraChrootDirs) { + if (line == "") { + state = stBegin; + } else { + auto p = line.find('='); + if (p == std::string::npos) + pathsInChroot[line] = line; + else + pathsInChroot[line.substr(0, p)] = line.substr(p + 1); + } + } + } + } + + return pathsInChroot; +} + +void DerivationBuilderImpl::prepareSandbox() +{ + if (drvOptions.useUidRange(drv)) + throw Error("feature 'uid-range' is not supported on this platform"); +} + +void DerivationBuilderImpl::openSlave() +{ + std::string slaveName = ptsname(builderOut.get()); + + AutoCloseFD builderOut = open(slaveName.c_str(), O_RDWR | O_NOCTTY); + if (!builderOut) + throw SysError("opening pseudoterminal slave"); + + // Put the pt into raw mode to prevent \n -> \r\n translation. + struct termios term; + if (tcgetattr(builderOut.get(), &term)) + throw SysError("getting pseudoterminal attributes"); + + cfmakeraw(&term); + + if (tcsetattr(builderOut.get(), TCSANOW, &term)) + throw SysError("putting pseudoterminal into raw mode"); + + if (dup2(builderOut.get(), STDERR_FILENO) == -1) + throw SysError("cannot pipe standard error into log file"); +} + +void DerivationBuilderImpl::startChild() +{ + pid = startProcess([&]() { + openSlave(); + runChild(); + }); +} + +void DerivationBuilderImpl::processSandboxSetupMessages() +{ + std::vector msgs; + while (true) { + std::string msg = [&]() { + try { + return readLine(builderOut.get()); + } catch (Error & e) { + auto status = pid.wait(); + e.addTrace({}, "while waiting for the build environment for '%s' to initialize (%s, previous messages: %s)", + store.printStorePath(drvPath), + statusToString(status), + concatStringsSep("|", msgs)); + throw; + } + }(); + if (msg.substr(0, 1) == "\2") break; + if (msg.substr(0, 1) == "\1") { + FdSource source(builderOut.get()); + auto ex = readError(source); + ex.addTrace({}, "while setting up the build environment"); + throw ex; + } + debug("sandbox setup: " + msg); + msgs.push_back(std::move(msg)); + } +} + +void DerivationBuilderImpl::initEnv() +{ + env.clear(); + + /* Most shells initialise PATH to some default (/bin:/usr/bin:...) when + PATH is not set. We don't want this, so we fill it in with some dummy + value. */ + env["PATH"] = "/path-not-set"; + + /* Set HOME to a non-existing path to prevent certain programs from using + /etc/passwd (or NIS, or whatever) to locate the home directory (for + example, wget looks for ~/.wgetrc). I.e., these tools use /etc/passwd + if HOME is not set, but they will just assume that the settings file + they are looking for does not exist if HOME is set but points to some + non-existing path. */ + env["HOME"] = homeDir; + + /* Tell the builder where the Nix store is. Usually they + shouldn't care, but this is useful for purity checking (e.g., + the compiler or linker might only want to accept paths to files + in the store or in the build directory). */ + env["NIX_STORE"] = store.storeDir; + + /* The maximum number of cores to utilize for parallel building. */ + env["NIX_BUILD_CORES"] = fmt("%d", settings.buildCores ? settings.buildCores : settings.getDefaultCores()); + + /* In non-structured mode, set all bindings either directory in the + environment or via a file, as specified by + `DerivationOptions::passAsFile`. */ + if (!parsedDrv) { + for (auto & i : drv.env) { + if (drvOptions.passAsFile.find(i.first) == drvOptions.passAsFile.end()) { + env[i.first] = i.second; + } else { + auto hash = hashString(HashAlgorithm::SHA256, i.first); + std::string fn = ".attr-" + hash.to_string(HashFormat::Nix32, false); + writeBuilderFile(fn, rewriteStrings(i.second, inputRewrites)); + env[i.first + "Path"] = tmpDirInSandbox() + "/" + fn; + } + } + } + + /* For convenience, set an environment pointing to the top build + directory. */ + env["NIX_BUILD_TOP"] = tmpDirInSandbox(); + + /* Also set TMPDIR and variants to point to this directory. */ + env["TMPDIR"] = env["TEMPDIR"] = env["TMP"] = env["TEMP"] = tmpDirInSandbox(); + + /* Explicitly set PWD to prevent problems with chroot builds. In + particular, dietlibc cannot figure out the cwd because the + inode of the current directory doesn't appear in .. (because + getdents returns the inode of the mount point). */ + env["PWD"] = tmpDirInSandbox(); + + /* Compatibility hack with Nix <= 0.7: if this is a fixed-output + derivation, tell the builder, so that for instance `fetchurl' + can skip checking the output. On older Nixes, this environment + variable won't be set, so `fetchurl' will do the check. */ + if (derivationType.isFixed()) env["NIX_OUTPUT_CHECKED"] = "1"; + + /* *Only* if this is a fixed-output derivation, propagate the + values of the environment variables specified in the + `impureEnvVars' attribute to the builder. This allows for + instance environment variables for proxy configuration such as + `http_proxy' to be easily passed to downloaders like + `fetchurl'. Passing such environment variables from the caller + to the builder is generally impure, but the output of + fixed-output derivations is by definition pure (since we + already know the cryptographic hash of the output). */ + if (!derivationType.isSandboxed()) { + auto & impureEnv = settings.impureEnv.get(); + if (!impureEnv.empty()) + experimentalFeatureSettings.require(Xp::ConfigurableImpureEnv); + + for (auto & i : drvOptions.impureEnvVars){ + auto envVar = impureEnv.find(i); + if (envVar != impureEnv.end()) { + env[i] = envVar->second; + } else { + env[i] = getEnv(i).value_or(""); + } + } + } + + /* Currently structured log messages piggyback on stderr, but we + may change that in the future. So tell the builder which file + descriptor to use for that. */ + env["NIX_LOG_FD"] = "2"; + + /* Trigger colored output in various tools. */ + env["TERM"] = "xterm-256color"; +} + + +void DerivationBuilderImpl::writeStructuredAttrs() +{ + if (parsedDrv) { + auto json = parsedDrv->prepareStructuredAttrs( + store, + drvOptions, + inputPaths, + drv.outputs); + nlohmann::json rewritten; + for (auto & [i, v] : json["outputs"].get()) { + /* The placeholder must have a rewrite, so we use it to cover both the + cases where we know or don't know the output path ahead of time. */ + rewritten[i] = rewriteStrings((std::string) v, inputRewrites); + } + + json["outputs"] = rewritten; + + auto jsonSh = StructuredAttrs::writeShell(json); + + writeBuilderFile(".attrs.sh", rewriteStrings(jsonSh, inputRewrites)); + env["NIX_ATTRS_SH_FILE"] = tmpDirInSandbox() + "/.attrs.sh"; + writeBuilderFile(".attrs.json", rewriteStrings(json.dump(), inputRewrites)); + env["NIX_ATTRS_JSON_FILE"] = tmpDirInSandbox() + "/.attrs.json"; + } +} + + +void DerivationBuilderImpl::startDaemon() +{ + experimentalFeatureSettings.require(Xp::RecursiveNix); + + auto store = makeRestrictedStore( + [&]{ + auto config = make_ref(*getLocalStore(this->store).config); + config->pathInfoCacheSize = 0; + config->stateDir = "/no-such-path"; + config->logDir = "/no-such-path"; + return config; + }(), + ref(std::dynamic_pointer_cast(this->store.shared_from_this())), + *this); + + addedPaths.clear(); + + auto socketName = ".nix-socket"; + Path socketPath = tmpDir + "/" + socketName; + env["NIX_REMOTE"] = "unix://" + tmpDirInSandbox() + "/" + socketName; + + daemonSocket = createUnixDomainSocket(socketPath, 0600); + + chownToBuilder(socketPath); + + daemonThread = std::thread([this, store]() { + + while (true) { + + /* Accept a connection. */ + struct sockaddr_un remoteAddr; + socklen_t remoteAddrLen = sizeof(remoteAddr); + + AutoCloseFD remote = accept(daemonSocket.get(), + (struct sockaddr *) &remoteAddr, &remoteAddrLen); + if (!remote) { + if (errno == EINTR || errno == EAGAIN) continue; + if (errno == EINVAL || errno == ECONNABORTED) break; + throw SysError("accepting connection"); + } + + unix::closeOnExec(remote.get()); + + debug("received daemon connection"); + + auto workerThread = std::thread([store, remote{std::move(remote)}]() { + try { + daemon::processConnection( + store, + FdSource(remote.get()), + FdSink(remote.get()), + NotTrusted, daemon::Recursive); + debug("terminated daemon connection"); + } catch (const Interrupted &) { + debug("interrupted daemon connection"); + } catch (SystemError &) { + ignoreExceptionExceptInterrupt(); + } + }); + + daemonWorkerThreads.push_back(std::move(workerThread)); + } + + debug("daemon shutting down"); + }); +} + + +void DerivationBuilderImpl::stopDaemon() +{ + if (daemonSocket && shutdown(daemonSocket.get(), SHUT_RDWR) == -1) { + // According to the POSIX standard, the 'shutdown' function should + // return an ENOTCONN error when attempting to shut down a socket that + // hasn't been connected yet. This situation occurs when the 'accept' + // function is called on a socket without any accepted connections, + // leaving the socket unconnected. While Linux doesn't seem to produce + // an error for sockets that have only been accepted, more + // POSIX-compliant operating systems like OpenBSD, macOS, and others do + // return the ENOTCONN error. Therefore, we handle this error here to + // avoid raising an exception for compliant behaviour. + if (errno == ENOTCONN) { + daemonSocket.close(); + } else { + throw SysError("shutting down daemon socket"); + } + } + + if (daemonThread.joinable()) + daemonThread.join(); + + // FIXME: should prune worker threads more quickly. + // FIXME: shutdown the client socket to speed up worker termination. + for (auto & thread : daemonWorkerThreads) + thread.join(); + daemonWorkerThreads.clear(); + + // release the socket. + daemonSocket.close(); +} + + +void DerivationBuilderImpl::addDependency(const StorePath & path) +{ + if (isAllowed(path)) return; + + addedPaths.insert(path); +} + +void DerivationBuilderImpl::chownToBuilder(const Path & path) +{ + if (!buildUser) return; + if (chown(path.c_str(), buildUser->getUID(), buildUser->getGID()) == -1) + throw SysError("cannot change ownership of '%1%'", path); +} + +void DerivationBuilderImpl::chownToBuilder(int fd, const Path & path) +{ + if (!buildUser) return; + if (fchown(fd, buildUser->getUID(), buildUser->getGID()) == -1) + throw SysError("cannot change ownership of file '%1%'", path); +} + +void DerivationBuilderImpl::writeBuilderFile( + const std::string & name, + std::string_view contents) +{ + auto path = std::filesystem::path(tmpDir) / name; + AutoCloseFD fd{openat(tmpDirFd.get(), name.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC | O_EXCL | O_NOFOLLOW, 0666)}; + if (!fd) + throw SysError("creating file %s", path); + writeFile(fd, path, contents); + chownToBuilder(fd.get(), path); +} + +void DerivationBuilderImpl::runChild() +{ + /* Warning: in the child we should absolutely not make any SQLite + calls! */ + + bool sendException = true; + + try { /* child */ + + commonChildInit(); + + /* Make the contents of netrc and the CA certificate bundle + available to builtin:fetchurl (which may run under a + different uid and/or in a sandbox). */ + BuiltinBuilderContext ctx{ + .drv = drv, + .tmpDirInSandbox = tmpDirInSandbox(), + }; + + if (drv.isBuiltin() && drv.builder == "builtin:fetchurl") { + try { + ctx.netrcData = readFile(settings.netrcFile); + } catch (SystemError &) { } + + try { + ctx.caFileData = readFile(settings.caFile); + } catch (SystemError &) { } + } + + enterChroot(); + + if (chdir(tmpDirInSandbox().c_str()) == -1) + throw SysError("changing into '%1%'", tmpDir); + + /* Close all other file descriptors. */ + unix::closeExtraFDs(); + + /* Disable core dumps by default. */ + struct rlimit limit = { 0, RLIM_INFINITY }; + setrlimit(RLIMIT_CORE, &limit); + + // FIXME: set other limits to deterministic values? + + setUser(); + + /* Indicate that we managed to set up the build environment. */ + writeFull(STDERR_FILENO, std::string("\2\n")); + + sendException = false; + + /* If this is a builtin builder, call it now. This should not return. */ + if (drv.isBuiltin()) { + try { + logger = makeJSONLogger(getStandardError()); + + for (auto & e : drv.outputs) + ctx.outputs.insert_or_assign(e.first, + store.printStorePath(scratchOutputs.at(e.first))); + + std::string builtinName = drv.builder.substr(8); + assert(RegisterBuiltinBuilder::builtinBuilders); + if (auto builtin = get(RegisterBuiltinBuilder::builtinBuilders(), builtinName)) + (*builtin)(ctx); + else + throw Error("unsupported builtin builder '%1%'", builtinName); + _exit(0); + } catch (std::exception & e) { + writeFull(STDERR_FILENO, e.what() + std::string("\n")); + _exit(1); + } + } + + /* It's not a builtin builder, so execute the program. */ + + Strings args; + args.push_back(std::string(baseNameOf(drv.builder))); + + for (auto & i : drv.args) + args.push_back(rewriteStrings(i, inputRewrites)); + + Strings envStrs; + for (auto & i : env) + envStrs.push_back(rewriteStrings(i.first + "=" + i.second, inputRewrites)); + + execBuilder(args, envStrs); + + throw SysError("executing '%1%'", drv.builder); + + } catch (...) { + handleChildException(sendException); + _exit(1); + } +} + +void DerivationBuilderImpl::setUser() +{ + /* If we are running in `build-users' mode, then switch to the + user we allocated above. Make sure that we drop all root + privileges. Note that above we have closed all file + descriptors except std*, so that's safe. Also note that + setuid() when run as root sets the real, effective and + saved UIDs. */ + if (buildUser) { + /* Preserve supplementary groups of the build user, to allow + admins to specify groups such as "kvm". */ + auto gids = buildUser->getSupplementaryGIDs(); + if (setgroups(gids.size(), gids.data()) == -1) + throw SysError("cannot set supplementary groups of build user"); + + if (setgid(buildUser->getGID()) == -1 || + getgid() != buildUser->getGID() || + getegid() != buildUser->getGID()) + throw SysError("setgid failed"); + + if (setuid(buildUser->getUID()) == -1 || + getuid() != buildUser->getUID() || + geteuid() != buildUser->getUID()) + throw SysError("setuid failed"); + } +} + +void DerivationBuilderImpl::execBuilder(const Strings & args, const Strings & envStrs) +{ + execve(drv.builder.c_str(), stringsToCharPtrs(args).data(), stringsToCharPtrs(envStrs).data()); +} + +SingleDrvOutputs DerivationBuilderImpl::registerOutputs() +{ + std::map infos; + + /* Set of inodes seen during calls to canonicalisePathMetaData() + for this build's outputs. This needs to be shared between + outputs to allow hard links between outputs. */ + InodesSeen inodesSeen; + + Path checkSuffix = ".check"; + + std::exception_ptr delayedException; + + /* The paths that can be referenced are the input closures, the + output paths, and any paths that have been built via recursive + Nix calls. */ + StorePathSet referenceablePaths; + for (auto & p : inputPaths) referenceablePaths.insert(p); + for (auto & i : scratchOutputs) referenceablePaths.insert(i.second); + for (auto & p : addedPaths) referenceablePaths.insert(p); + + /* Check whether the output paths were created, and make all + output paths read-only. Then get the references of each output (that we + might need to register), so we can topologically sort them. For the ones + that are most definitely already installed, we just store their final + name so we can also use it in rewrites. */ + StringSet outputsToSort; + struct AlreadyRegistered { StorePath path; }; + struct PerhapsNeedToRegister { StorePathSet refs; }; + std::map> outputReferencesIfUnregistered; + std::map outputStats; + for (auto & [outputName, _] : drv.outputs) { + auto scratchOutput = get(scratchOutputs, outputName); + if (!scratchOutput) + throw BuildError( + "builder for '%s' has no scratch output for '%s'", + store.printStorePath(drvPath), outputName); + auto actualPath = realPathInSandbox(store.printStorePath(*scratchOutput)); + + outputsToSort.insert(outputName); + + /* Updated wanted info to remove the outputs we definitely don't need to register */ + auto initialOutput = get(initialOutputs, outputName); + if (!initialOutput) + throw BuildError( + "builder for '%s' has no initial output for '%s'", + store.printStorePath(drvPath), outputName); + auto & initialInfo = *initialOutput; + + /* Don't register if already valid, and not checking */ + initialInfo.wanted = buildMode == bmCheck + || !(initialInfo.known && initialInfo.known->isValid()); + if (!initialInfo.wanted) { + outputReferencesIfUnregistered.insert_or_assign( + outputName, + AlreadyRegistered { .path = initialInfo.known->path }); + continue; + } + + auto optSt = maybeLstat(actualPath.c_str()); + if (!optSt) + throw BuildError( + "builder for '%s' failed to produce output path for output '%s' at '%s'", + store.printStorePath(drvPath), outputName, actualPath); + struct stat & st = *optSt; + +#ifndef __CYGWIN__ + /* Check that the output is not group or world writable, as + that means that someone else can have interfered with the + build. Also, the output should be owned by the build + user. */ + if ((!S_ISLNK(st.st_mode) && (st.st_mode & (S_IWGRP | S_IWOTH))) || + (buildUser && st.st_uid != buildUser->getUID())) + throw BuildError( + "suspicious ownership or permission on '%s' for output '%s'; rejecting this build output", + actualPath, outputName); +#endif + + /* Canonicalise first. This ensures that the path we're + rewriting doesn't contain a hard link to /etc/shadow or + something like that. */ + canonicalisePathMetaData( + actualPath, + buildUser ? std::optional(buildUser->getUIDRange()) : std::nullopt, + inodesSeen); + + bool discardReferences = false; + if (auto udr = get(drvOptions.unsafeDiscardReferences, outputName)) { + discardReferences = *udr; + } + + StorePathSet references; + if (discardReferences) + debug("discarding references of output '%s'", outputName); + else { + debug("scanning for references for output '%s' in temp location '%s'", outputName, actualPath); + + /* Pass blank Sink as we are not ready to hash data at this stage. */ + NullSink blank; + references = scanForReferences(blank, actualPath, referenceablePaths); + } + + outputReferencesIfUnregistered.insert_or_assign( + outputName, + PerhapsNeedToRegister { .refs = references }); + outputStats.insert_or_assign(outputName, std::move(st)); + } + + auto sortedOutputNames = topoSort(outputsToSort, + {[&](const std::string & name) { + auto orifu = get(outputReferencesIfUnregistered, name); + if (!orifu) + throw BuildError( + "no output reference for '%s' in build of '%s'", + name, store.printStorePath(drvPath)); + return std::visit(overloaded { + /* Since we'll use the already installed versions of these, we + can treat them as leaves and ignore any references they + have. */ + [&](const AlreadyRegistered &) { return StringSet {}; }, + [&](const PerhapsNeedToRegister & refs) { + StringSet referencedOutputs; + /* FIXME build inverted map up front so no quadratic waste here */ + for (auto & r : refs.refs) + for (auto & [o, p] : scratchOutputs) + if (r == p) + referencedOutputs.insert(o); + return referencedOutputs; + }, + }, *orifu); + }}, + {[&](const std::string & path, const std::string & parent) { + // TODO with more -vvvv also show the temporary paths for manual inspection. + return BuildError( + "cycle detected in build of '%s' in the references of output '%s' from output '%s'", + store.printStorePath(drvPath), path, parent); + }}); + + std::reverse(sortedOutputNames.begin(), sortedOutputNames.end()); + + OutputPathMap finalOutputs; + + for (auto & outputName : sortedOutputNames) { + auto output = get(drv.outputs, outputName); + auto scratchPath = get(scratchOutputs, outputName); + assert(output && scratchPath); + auto actualPath = realPathInSandbox(store.printStorePath(*scratchPath)); + + auto finish = [&](StorePath finalStorePath) { + /* Store the final path */ + finalOutputs.insert_or_assign(outputName, finalStorePath); + /* The rewrite rule will be used in downstream outputs that refer to + use. This is why the topological sort is essential to do first + before this for loop. */ + if (*scratchPath != finalStorePath) + outputRewrites[std::string { scratchPath->hashPart() }] = std::string { finalStorePath.hashPart() }; + }; + + auto orifu = get(outputReferencesIfUnregistered, outputName); + assert(orifu); + + std::optional referencesOpt = std::visit(overloaded { + [&](const AlreadyRegistered & skippedFinalPath) -> std::optional { + finish(skippedFinalPath.path); + return std::nullopt; + }, + [&](const PerhapsNeedToRegister & r) -> std::optional { + return r.refs; + }, + }, *orifu); + + if (!referencesOpt) + continue; + auto references = *referencesOpt; + + auto rewriteOutput = [&](const StringMap & rewrites) { + /* Apply hash rewriting if necessary. */ + if (!rewrites.empty()) { + debug("rewriting hashes in '%1%'; cross fingers", actualPath); + + /* FIXME: Is this actually streaming? */ + auto source = sinkToSource([&](Sink & nextSink) { + RewritingSink rsink(rewrites, nextSink); + dumpPath(actualPath, rsink); + rsink.flush(); + }); + Path tmpPath = actualPath + ".tmp"; + restorePath(tmpPath, *source); + deletePath(actualPath); + movePath(tmpPath, actualPath); + + /* FIXME: set proper permissions in restorePath() so + we don't have to do another traversal. */ + canonicalisePathMetaData(actualPath, {}, inodesSeen); + } + }; + + auto rewriteRefs = [&]() -> StoreReferences { + /* In the CA case, we need the rewritten refs to calculate the + final path, therefore we look for a *non-rewritten + self-reference, and use a bool rather try to solve the + computationally intractable fixed point. */ + StoreReferences res { + .self = false, + }; + for (auto & r : references) { + auto name = r.name(); + auto origHash = std::string { r.hashPart() }; + if (r == *scratchPath) { + res.self = true; + } else if (auto outputRewrite = get(outputRewrites, origHash)) { + std::string newRef = *outputRewrite; + newRef += '-'; + newRef += name; + res.others.insert(StorePath { newRef }); + } else { + res.others.insert(r); + } + } + return res; + }; + + auto newInfoFromCA = [&](const DerivationOutput::CAFloating outputHash) -> ValidPathInfo { + auto st = get(outputStats, outputName); + if (!st) + throw BuildError( + "output path %1% without valid stats info", + actualPath); + if (outputHash.method.getFileIngestionMethod() == FileIngestionMethod::Flat) + { + /* The output path should be a regular file without execute permission. */ + if (!S_ISREG(st->st_mode) || (st->st_mode & S_IXUSR) != 0) + throw BuildError( + "output path '%1%' should be a non-executable regular file " + "since recursive hashing is not enabled (one of outputHashMode={flat,text} is true)", + actualPath); + } + rewriteOutput(outputRewrites); + /* FIXME optimize and deduplicate with addToStore */ + std::string oldHashPart { scratchPath->hashPart() }; + auto got = [&]{ + auto fim = outputHash.method.getFileIngestionMethod(); + switch (fim) { + case FileIngestionMethod::Flat: + case FileIngestionMethod::NixArchive: + { + HashModuloSink caSink { outputHash.hashAlgo, oldHashPart }; + auto fim = outputHash.method.getFileIngestionMethod(); + dumpPath( + {getFSSourceAccessor(), CanonPath(actualPath)}, + caSink, + (FileSerialisationMethod) fim); + return caSink.finish().first; + } + case FileIngestionMethod::Git: { + return git::dumpHash( + outputHash.hashAlgo, + {getFSSourceAccessor(), CanonPath(actualPath)}).hash; + } + } + assert(false); + }(); + + ValidPathInfo newInfo0 { + store, + outputPathName(drv.name, outputName), + ContentAddressWithReferences::fromParts( + outputHash.method, + std::move(got), + rewriteRefs()), + Hash::dummy, + }; + if (*scratchPath != newInfo0.path) { + // If the path has some self-references, we need to rewrite + // them. + // (note that this doesn't invalidate the ca hash we calculated + // above because it's computed *modulo the self-references*, so + // it already takes this rewrite into account). + rewriteOutput( + StringMap{{oldHashPart, + std::string(newInfo0.path.hashPart())}}); + } + + { + HashResult narHashAndSize = hashPath( + {getFSSourceAccessor(), CanonPath(actualPath)}, + FileSerialisationMethod::NixArchive, HashAlgorithm::SHA256); + newInfo0.narHash = narHashAndSize.first; + newInfo0.narSize = narHashAndSize.second; + } + + assert(newInfo0.ca); + return newInfo0; + }; + + ValidPathInfo newInfo = std::visit(overloaded { + + [&](const DerivationOutput::InputAddressed & output) { + /* input-addressed case */ + auto requiredFinalPath = output.path; + /* Preemptively add rewrite rule for final hash, as that is + what the NAR hash will use rather than normalized-self references */ + if (*scratchPath != requiredFinalPath) + outputRewrites.insert_or_assign( + std::string { scratchPath->hashPart() }, + std::string { requiredFinalPath.hashPart() }); + rewriteOutput(outputRewrites); + HashResult narHashAndSize = hashPath( + {getFSSourceAccessor(), CanonPath(actualPath)}, + FileSerialisationMethod::NixArchive, HashAlgorithm::SHA256); + ValidPathInfo newInfo0 { requiredFinalPath, narHashAndSize.first }; + newInfo0.narSize = narHashAndSize.second; + auto refs = rewriteRefs(); + newInfo0.references = std::move(refs.others); + if (refs.self) + newInfo0.references.insert(newInfo0.path); + return newInfo0; + }, + + [&](const DerivationOutput::CAFixed & dof) { + auto & wanted = dof.ca.hash; + + // Replace the output by a fresh copy of itself to make sure + // that there's no stale file descriptor pointing to it + Path tmpOutput = actualPath + ".tmp"; + copyFile( + std::filesystem::path(actualPath), + std::filesystem::path(tmpOutput), true); + + std::filesystem::rename(tmpOutput, actualPath); + + auto newInfo0 = newInfoFromCA(DerivationOutput::CAFloating { + .method = dof.ca.method, + .hashAlgo = wanted.algo, + }); + + /* Check wanted hash */ + assert(newInfo0.ca); + auto & got = newInfo0.ca->hash; + if (wanted != got) { + /* Throw an error after registering the path as + valid. */ + miscMethods->noteHashMismatch(); + delayedException = std::make_exception_ptr( + BuildError("hash mismatch in fixed-output derivation '%s':\n specified: %s\n got: %s", + store.printStorePath(drvPath), + wanted.to_string(HashFormat::SRI, true), + got.to_string(HashFormat::SRI, true))); + } + if (!newInfo0.references.empty()) { + auto numViolations = newInfo.references.size(); + delayedException = std::make_exception_ptr( + BuildError("fixed-output derivations must not reference store paths: '%s' references %d distinct paths, e.g. '%s'", + store.printStorePath(drvPath), + numViolations, + store.printStorePath(*newInfo.references.begin()))); + } + + return newInfo0; + }, + + [&](const DerivationOutput::CAFloating & dof) { + return newInfoFromCA(dof); + }, + + [&](const DerivationOutput::Deferred &) -> ValidPathInfo { + // No derivation should reach that point without having been + // rewritten first + assert(false); + }, + + [&](const DerivationOutput::Impure & doi) { + return newInfoFromCA(DerivationOutput::CAFloating { + .method = doi.method, + .hashAlgo = doi.hashAlgo, + }); + }, + + }, output->raw); + + /* FIXME: set proper permissions in restorePath() so + we don't have to do another traversal. */ + canonicalisePathMetaData(actualPath, {}, inodesSeen); + + /* Calculate where we'll move the output files. In the checking case we + will leave leave them where they are, for now, rather than move to + their usual "final destination" */ + auto finalDestPath = store.printStorePath(newInfo.path); + + /* Lock final output path, if not already locked. This happens with + floating CA derivations and hash-mismatching fixed-output + derivations. */ + PathLocks dynamicOutputLock; + dynamicOutputLock.setDeletion(true); + auto optFixedPath = output->path(store, drv.name, outputName); + if (!optFixedPath || + store.printStorePath(*optFixedPath) != finalDestPath) + { + assert(newInfo.ca); + dynamicOutputLock.lockPaths({store.toRealPath(finalDestPath)}); + } + + /* Move files, if needed */ + if (store.toRealPath(finalDestPath) != actualPath) { + if (buildMode == bmRepair) { + /* Path already exists, need to replace it */ + replaceValidPath(store.toRealPath(finalDestPath), actualPath); + actualPath = store.toRealPath(finalDestPath); + } else if (buildMode == bmCheck) { + /* Path already exists, and we want to compare, so we leave out + new path in place. */ + } else if (store.isValidPath(newInfo.path)) { + /* Path already exists because CA path produced by something + else. No moving needed. */ + assert(newInfo.ca); + } else { + auto destPath = store.toRealPath(finalDestPath); + deletePath(destPath); + movePath(actualPath, destPath); + actualPath = destPath; + } + } + + auto & localStore = getLocalStore(store); + + if (buildMode == bmCheck) { + + if (!store.isValidPath(newInfo.path)) continue; + ValidPathInfo oldInfo(*store.queryPathInfo(newInfo.path)); + if (newInfo.narHash != oldInfo.narHash) { + miscMethods->noteCheckMismatch(); + if (settings.runDiffHook || settings.keepFailed) { + auto dst = store.toRealPath(finalDestPath + checkSuffix); + deletePath(dst); + movePath(actualPath, dst); + + handleDiffHook( + buildUser ? buildUser->getUID() : getuid(), + buildUser ? buildUser->getGID() : getgid(), + finalDestPath, dst, store.printStorePath(drvPath), tmpDir); + + throw NotDeterministic("derivation '%s' may not be deterministic: output '%s' differs from '%s'", + store.printStorePath(drvPath), store.toRealPath(finalDestPath), dst); + } else + throw NotDeterministic("derivation '%s' may not be deterministic: output '%s' differs", + store.printStorePath(drvPath), store.toRealPath(finalDestPath)); + } + + /* Since we verified the build, it's now ultimately trusted. */ + if (!oldInfo.ultimate) { + oldInfo.ultimate = true; + localStore.signPathInfo(oldInfo); + localStore.registerValidPaths({{oldInfo.path, oldInfo}}); + } + + continue; + } + + /* For debugging, print out the referenced and unreferenced paths. */ + for (auto & i : inputPaths) { + if (references.count(i)) + debug("referenced input: '%1%'", store.printStorePath(i)); + else + debug("unreferenced input: '%1%'", store.printStorePath(i)); + } + + localStore.optimisePath(actualPath, NoRepair); // FIXME: combine with scanForReferences() + miscMethods->markContentsGood(newInfo.path); + + newInfo.deriver = drvPath; + newInfo.ultimate = true; + localStore.signPathInfo(newInfo); + + finish(newInfo.path); + + /* If it's a CA path, register it right away. This is necessary if it + isn't statically known so that we can safely unlock the path before + the next iteration */ + if (newInfo.ca) + localStore.registerValidPaths({{newInfo.path, newInfo}}); + + infos.emplace(outputName, std::move(newInfo)); + } + + if (buildMode == bmCheck) { + /* In case of fixed-output derivations, if there are + mismatches on `--check` an error must be thrown as this is + also a source for non-determinism. */ + if (delayedException) + std::rethrow_exception(delayedException); + return {}; + } + + /* Apply output checks. */ + checkOutputs(infos); + + /* Register each output path as valid, and register the sets of + paths referenced by each of them. If there are cycles in the + outputs, this will fail. */ + { + auto & localStore = getLocalStore(store); + + ValidPathInfos infos2; + for (auto & [outputName, newInfo] : infos) { + infos2.insert_or_assign(newInfo.path, newInfo); + } + localStore.registerValidPaths(infos2); + } + + /* In case of a fixed-output derivation hash mismatch, throw an + exception now that we have registered the output as valid. */ + if (delayedException) + std::rethrow_exception(delayedException); + + /* If we made it this far, we are sure the output matches the derivation + (since the delayedException would be a fixed output CA mismatch). That + means it's safe to link the derivation to the output hash. We must do + that for floating CA derivations, which otherwise couldn't be cached, + but it's fine to do in all cases. */ + SingleDrvOutputs builtOutputs; + + for (auto & [outputName, newInfo] : infos) { + auto oldinfo = get(initialOutputs, outputName); + assert(oldinfo); + auto thisRealisation = Realisation { + .id = DrvOutput { + oldinfo->outputHash, + outputName + }, + .outPath = newInfo.path + }; + if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations) + && !drv.type().isImpure()) + { + store.signRealisation(thisRealisation); + store.registerDrvOutput(thisRealisation); + } + builtOutputs.emplace(outputName, thisRealisation); + } + + return builtOutputs; +} + + +void DerivationBuilderImpl::checkOutputs(const std::map & outputs) +{ + std::map outputsByPath; + for (auto & output : outputs) + outputsByPath.emplace(store.printStorePath(output.second.path), output.second); + + for (auto & output : outputs) { + auto & outputName = output.first; + auto & info = output.second; + + /* Compute the closure and closure size of some output. This + is slightly tricky because some of its references (namely + other outputs) may not be valid yet. */ + auto getClosure = [&](const StorePath & path) + { + uint64_t closureSize = 0; + StorePathSet pathsDone; + std::queue pathsLeft; + pathsLeft.push(path); + + while (!pathsLeft.empty()) { + auto path = pathsLeft.front(); + pathsLeft.pop(); + if (!pathsDone.insert(path).second) continue; + + auto i = outputsByPath.find(store.printStorePath(path)); + if (i != outputsByPath.end()) { + closureSize += i->second.narSize; + for (auto & ref : i->second.references) + pathsLeft.push(ref); + } else { + auto info = store.queryPathInfo(path); + closureSize += info->narSize; + for (auto & ref : info->references) + pathsLeft.push(ref); + } + } + + return std::make_pair(std::move(pathsDone), closureSize); + }; + + auto applyChecks = [&](const DerivationOptions::OutputChecks & checks) + { + if (checks.maxSize && info.narSize > *checks.maxSize) + throw BuildError("path '%s' is too large at %d bytes; limit is %d bytes", + store.printStorePath(info.path), info.narSize, *checks.maxSize); + + if (checks.maxClosureSize) { + uint64_t closureSize = getClosure(info.path).second; + if (closureSize > *checks.maxClosureSize) + throw BuildError("closure of path '%s' is too large at %d bytes; limit is %d bytes", + store.printStorePath(info.path), closureSize, *checks.maxClosureSize); + } + + auto checkRefs = [&](const StringSet & value, bool allowed, bool recursive) + { + /* Parse a list of reference specifiers. Each element must + either be a store path, or the symbolic name of the output + of the derivation (such as `out'). */ + StorePathSet spec; + for (auto & i : value) { + if (store.isStorePath(i)) + spec.insert(store.parseStorePath(i)); + else if (auto output = get(outputs, i)) + spec.insert(output->path); + else { + std::string outputsListing = concatMapStringsSep(", ", outputs, [](auto & o) { return o.first; }); + throw BuildError("derivation '%s' output check for '%s' contains an illegal reference specifier '%s'," + " expected store path or output name (one of [%s])", + store.printStorePath(drvPath), outputName, i, outputsListing); + } + } + + auto used = recursive + ? getClosure(info.path).first + : info.references; + + if (recursive && checks.ignoreSelfRefs) + used.erase(info.path); + + StorePathSet badPaths; + + for (auto & i : used) + if (allowed) { + if (!spec.count(i)) + badPaths.insert(i); + } else { + if (spec.count(i)) + badPaths.insert(i); + } + + if (!badPaths.empty()) { + std::string badPathsStr; + for (auto & i : badPaths) { + badPathsStr += "\n "; + badPathsStr += store.printStorePath(i); + } + throw BuildError("output '%s' is not allowed to refer to the following paths:%s", + store.printStorePath(info.path), badPathsStr); + } + }; + + /* Mandatory check: absent whitelist, and present but empty + whitelist mean very different things. */ + if (auto & refs = checks.allowedReferences) { + checkRefs(*refs, true, false); + } + if (auto & refs = checks.allowedRequisites) { + checkRefs(*refs, true, true); + } + + /* Optimization: don't need to do anything when + disallowed and empty set. */ + if (!checks.disallowedReferences.empty()) { + checkRefs(checks.disallowedReferences, false, false); + } + if (!checks.disallowedRequisites.empty()) { + checkRefs(checks.disallowedRequisites, false, true); + } + }; + + std::visit(overloaded{ + [&](const DerivationOptions::OutputChecks & checks) { + applyChecks(checks); + }, + [&](const std::map & checksPerOutput) { + if (auto outputChecks = get(checksPerOutput, outputName)) + + applyChecks(*outputChecks); + }, + }, drvOptions.outputChecks); + } +} + + +void DerivationBuilderImpl::deleteTmpDir(bool force) +{ + if (topTmpDir != "") { + /* As an extra precaution, even in the event of `deletePath` failing to + * clean up, the `tmpDir` will be chowned as if we were to move + * it inside the Nix store. + * + * This hardens against an attack which smuggles a file descriptor + * to make use of the temporary directory. + */ + chmod(topTmpDir.c_str(), 0000); + + /* Don't keep temporary directories for builtins because they + might have privileged stuff (like a copy of netrc). */ + if (settings.keepFailed && !force && !drv.isBuiltin()) { + printError("note: keeping build directory '%s'", tmpDir); + chmod(topTmpDir.c_str(), 0755); + chmod(tmpDir.c_str(), 0755); + } + else + deletePath(topTmpDir); + topTmpDir = ""; + tmpDir = ""; + } +} + + +StorePath DerivationBuilderImpl::makeFallbackPath(OutputNameView outputName) +{ + // This is a bogus path type, constructed this way to ensure that it doesn't collide with any other store path + // See doc/manual/source/protocols/store-path.md for details + // TODO: We may want to separate the responsibilities of constructing the path fingerprint and of actually doing the hashing + auto pathType = "rewrite:" + std::string(drvPath.to_string()) + ":name:" + std::string(outputName); + return store.makeStorePath( + pathType, + // pass an all-zeroes hash + Hash(HashAlgorithm::SHA256), outputPathName(drv.name, outputName)); +} + + +StorePath DerivationBuilderImpl::makeFallbackPath(const StorePath & path) +{ + // This is a bogus path type, constructed this way to ensure that it doesn't collide with any other store path + // See doc/manual/source/protocols/store-path.md for details + auto pathType = "rewrite:" + std::string(drvPath.to_string()) + ":" + std::string(path.to_string()); + return store.makeStorePath( + pathType, + // pass an all-zeroes hash + Hash(HashAlgorithm::SHA256), path.name()); +} + +} + +// FIXME: do this properly +#include "linux-derivation-builder.cc" +#include "darwin-derivation-builder.cc" + +namespace nix { + +std::unique_ptr makeDerivationBuilder( + Store & store, + std::unique_ptr miscMethods, + DerivationBuilderParams params) +{ + bool useSandbox = false; + + /* Are we doing a sandboxed build? */ + { + if (settings.sandboxMode == smEnabled) { + if (params.drvOptions.noChroot) + throw Error("derivation '%s' has '__noChroot' set, " + "but that's not allowed when 'sandbox' is 'true'", store.printStorePath(params.drvPath)); +#ifdef __APPLE__ + if (params.drvOptions.additionalSandboxProfile != "") + throw Error("derivation '%s' specifies a sandbox profile, " + "but this is only allowed when 'sandbox' is 'relaxed'", store.printStorePath(params.drvPath)); +#endif + useSandbox = true; + } + else if (settings.sandboxMode == smDisabled) + useSandbox = false; + else if (settings.sandboxMode == smRelaxed) + // FIXME: cache derivationType + useSandbox = params.drv.type().isSandboxed() && !params.drvOptions.noChroot; + } + + auto & localStore = getLocalStore(store); + if (localStore.storeDir != localStore.config->realStoreDir.get()) { + #ifdef __linux__ + useSandbox = true; + #else + throw Error("building using a diverted store is not supported on this platform"); + #endif + } + + #ifdef __linux__ + if (useSandbox && !mountAndPidNamespacesSupported()) { + if (!settings.sandboxFallback) + throw Error("this system does not support the kernel namespaces that are required for sandboxing; use '--no-sandbox' to disable sandboxing"); + debug("auto-disabling sandboxing because the prerequisite namespaces are not available"); + useSandbox = false; + } + + if (useSandbox) + return std::make_unique( + store, + std::move(miscMethods), + std::move(params)); + #endif + + if (!useSandbox && params.drvOptions.useUidRange(params.drv)) + throw Error("feature 'uid-range' is only supported in sandboxed builds"); + + #ifdef __APPLE__ + return std::make_unique( + store, + std::move(miscMethods), + std::move(params), + useSandbox); + #elif defined(__linux__) + return std::make_unique( + store, + std::move(miscMethods), + std::move(params)); + #else + if (useSandbox) + throw Error("sandboxing builds is not supported on this platform"); + + return std::make_unique( + store, + std::move(miscMethods), + std::move(params)); + #endif +} + +} diff --git a/src/libstore/unix/build/hook-instance.cc b/src/libstore/unix/build/hook-instance.cc index 79eb25a91be..3713f7c86e6 100644 --- a/src/libstore/unix/build/hook-instance.cc +++ b/src/libstore/unix/build/hook-instance.cc @@ -1,10 +1,10 @@ -#include "globals.hh" -#include "config-global.hh" -#include "hook-instance.hh" -#include "file-system.hh" -#include "child.hh" -#include "strings.hh" -#include "executable-path.hh" +#include "nix/store/globals.hh" +#include "nix/util/config-global.hh" +#include "nix/store/build/hook-instance.hh" +#include "nix/util/file-system.hh" +#include "nix/store/build/child.hh" +#include "nix/util/strings.hh" +#include "nix/util/executable-path.hh" namespace nix { diff --git a/src/libstore/unix/build/linux-derivation-builder.cc b/src/libstore/unix/build/linux-derivation-builder.cc new file mode 100644 index 00000000000..b23c8003f5c --- /dev/null +++ b/src/libstore/unix/build/linux-derivation-builder.cc @@ -0,0 +1,883 @@ +#ifdef __linux__ + +# include "nix/store/personality.hh" +# include "nix/util/cgroup.hh" +# include "nix/util/linux-namespaces.hh" +# include "linux/fchmodat2-compat.hh" + +# include +# include +# include +# include +# include +# include +# include +# include + +# if HAVE_SECCOMP +# include +# endif + +# define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root, put_old)) + +namespace nix { + +static void setupSeccomp() +{ + if (!settings.filterSyscalls) + return; + +# if HAVE_SECCOMP + scmp_filter_ctx ctx; + + if (!(ctx = seccomp_init(SCMP_ACT_ALLOW))) + throw SysError("unable to initialize seccomp mode 2"); + + Finally cleanup([&]() { seccomp_release(ctx); }); + + constexpr std::string_view nativeSystem = NIX_LOCAL_SYSTEM; + + if (nativeSystem == "x86_64-linux" && seccomp_arch_add(ctx, SCMP_ARCH_X86) != 0) + throw SysError("unable to add 32-bit seccomp architecture"); + + if (nativeSystem == "x86_64-linux" && seccomp_arch_add(ctx, SCMP_ARCH_X32) != 0) + throw SysError("unable to add X32 seccomp architecture"); + + if (nativeSystem == "aarch64-linux" && seccomp_arch_add(ctx, SCMP_ARCH_ARM) != 0) + printError( + "unable to add ARM seccomp architecture; this may result in spurious build failures if running 32-bit ARM processes"); + + if (nativeSystem == "mips64-linux" && seccomp_arch_add(ctx, SCMP_ARCH_MIPS) != 0) + printError("unable to add mips seccomp architecture"); + + if (nativeSystem == "mips64-linux" && seccomp_arch_add(ctx, SCMP_ARCH_MIPS64N32) != 0) + printError("unable to add mips64-*abin32 seccomp architecture"); + + if (nativeSystem == "mips64el-linux" && seccomp_arch_add(ctx, SCMP_ARCH_MIPSEL) != 0) + printError("unable to add mipsel seccomp architecture"); + + if (nativeSystem == "mips64el-linux" && seccomp_arch_add(ctx, SCMP_ARCH_MIPSEL64N32) != 0) + printError("unable to add mips64el-*abin32 seccomp architecture"); + + /* Prevent builders from creating setuid/setgid binaries. */ + for (int perm : {S_ISUID, S_ISGID}) { + if (seccomp_rule_add( + ctx, + SCMP_ACT_ERRNO(EPERM), + SCMP_SYS(chmod), + 1, + SCMP_A1(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) + != 0) + throw SysError("unable to add seccomp rule"); + + if (seccomp_rule_add( + ctx, + SCMP_ACT_ERRNO(EPERM), + SCMP_SYS(fchmod), + 1, + SCMP_A1(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) + != 0) + throw SysError("unable to add seccomp rule"); + + if (seccomp_rule_add( + ctx, + SCMP_ACT_ERRNO(EPERM), + SCMP_SYS(fchmodat), + 1, + SCMP_A2(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) + != 0) + throw SysError("unable to add seccomp rule"); + + if (seccomp_rule_add( + ctx, + SCMP_ACT_ERRNO(EPERM), + NIX_SYSCALL_FCHMODAT2, + 1, + SCMP_A2(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) + != 0) + throw SysError("unable to add seccomp rule"); + } + + /* Prevent builders from using EAs or ACLs. Not all filesystems + support these, and they're not allowed in the Nix store because + they're not representable in the NAR serialisation. */ + if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(getxattr), 0) != 0 + || seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(lgetxattr), 0) != 0 + || seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(fgetxattr), 0) != 0 + || seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(setxattr), 0) != 0 + || seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(lsetxattr), 0) != 0 + || seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(fsetxattr), 0) != 0) + throw SysError("unable to add seccomp rule"); + + if (seccomp_attr_set(ctx, SCMP_FLTATR_CTL_NNP, settings.allowNewPrivileges ? 0 : 1) != 0) + throw SysError("unable to set 'no new privileges' seccomp attribute"); + + if (seccomp_load(ctx) != 0) + throw SysError("unable to load seccomp BPF program"); +# else + throw Error( + "seccomp is not supported on this platform; " + "you can bypass this error by setting the option 'filter-syscalls' to false, but note that untrusted builds can then create setuid binaries!"); +# endif +} + +static void doBind(const Path & source, const Path & target, bool optional = false) +{ + debug("bind mounting '%1%' to '%2%'", source, target); + + auto bindMount = [&]() { + if (mount(source.c_str(), target.c_str(), "", MS_BIND | MS_REC, 0) == -1) + throw SysError("bind mount from '%1%' to '%2%' failed", source, target); + }; + + auto maybeSt = maybeLstat(source); + if (!maybeSt) { + if (optional) + return; + else + throw SysError("getting attributes of path '%1%'", source); + } + auto st = *maybeSt; + + if (S_ISDIR(st.st_mode)) { + createDirs(target); + bindMount(); + } else if (S_ISLNK(st.st_mode)) { + // Symlinks can (apparently) not be bind-mounted, so just copy it + createDirs(dirOf(target)); + copyFile(std::filesystem::path(source), std::filesystem::path(target), false); + } else { + createDirs(dirOf(target)); + writeFile(target, ""); + bindMount(); + } +} + +struct LinuxDerivationBuilder : DerivationBuilderImpl +{ + using DerivationBuilderImpl::DerivationBuilderImpl; + + void enterChroot() override + { + setupSeccomp(); + + linux::setPersonality(drv.platform); + } +}; + +struct ChrootLinuxDerivationBuilder : LinuxDerivationBuilder +{ + /** + * Pipe for synchronising updates to the builder namespaces. + */ + Pipe userNamespaceSync; + + /** + * The mount namespace and user namespace of the builder, used to add additional + * paths to the sandbox as a result of recursive Nix calls. + */ + AutoCloseFD sandboxMountNamespace; + AutoCloseFD sandboxUserNamespace; + + /** + * On Linux, whether we're doing the build in its own user + * namespace. + */ + bool usingUserNamespace = true; + + /** + * The root of the chroot environment. + */ + Path chrootRootDir; + + /** + * RAII object to delete the chroot directory. + */ + std::shared_ptr autoDelChroot; + + PathsInChroot pathsInChroot; + + /** + * The cgroup of the builder, if any. + */ + std::optional cgroup; + + using LinuxDerivationBuilder::LinuxDerivationBuilder; + + void deleteTmpDir(bool force) override + { + autoDelChroot.reset(); /* this runs the destructor */ + + DerivationBuilderImpl::deleteTmpDir(force); + } + + uid_t sandboxUid() + { + return usingUserNamespace ? (!buildUser || buildUser->getUIDCount() == 1 ? 1000 : 0) : buildUser->getUID(); + } + + gid_t sandboxGid() + { + return usingUserNamespace ? (!buildUser || buildUser->getUIDCount() == 1 ? 100 : 0) : buildUser->getGID(); + } + + bool needsHashRewrite() override + { + return false; + } + + std::unique_ptr getBuildUser() override + { + return acquireUserLock(drvOptions.useUidRange(drv) ? 65536 : 1, true); + } + + void setBuildTmpDir() override + { + /* If sandboxing is enabled, put the actual TMPDIR underneath + an inaccessible root-owned directory, to prevent outside + access. + + On macOS, we don't use an actual chroot, so this isn't + possible. Any mitigation along these lines would have to be + done directly in the sandbox profile. */ + tmpDir = topTmpDir + "/build"; + createDir(tmpDir, 0700); + } + + Path tmpDirInSandbox() override + { + /* In a sandbox, for determinism, always use the same temporary + directory. */ + return settings.sandboxBuildDir; + } + + void prepareUser() override + { + if ((buildUser && buildUser->getUIDCount() != 1) || settings.useCgroups) { + experimentalFeatureSettings.require(Xp::Cgroups); + + /* If we're running from the daemon, then this will return the + root cgroup of the service. Otherwise, it will return the + current cgroup. */ + auto rootCgroup = getRootCgroup(); + auto cgroupFS = getCgroupFS(); + if (!cgroupFS) + throw Error("cannot determine the cgroups file system"); + auto rootCgroupPath = canonPath(*cgroupFS + "/" + rootCgroup); + if (!pathExists(rootCgroupPath)) + throw Error("expected cgroup directory '%s'", rootCgroupPath); + + static std::atomic counter{0}; + + cgroup = buildUser ? fmt("%s/nix-build-uid-%d", rootCgroupPath, buildUser->getUID()) + : fmt("%s/nix-build-pid-%d-%d", rootCgroupPath, getpid(), counter++); + + debug("using cgroup '%s'", *cgroup); + + /* When using a build user, record the cgroup we used for that + user so that if we got interrupted previously, we can kill + any left-over cgroup first. */ + if (buildUser) { + auto cgroupsDir = settings.nixStateDir + "/cgroups"; + createDirs(cgroupsDir); + + auto cgroupFile = fmt("%s/%d", cgroupsDir, buildUser->getUID()); + + if (pathExists(cgroupFile)) { + auto prevCgroup = readFile(cgroupFile); + destroyCgroup(prevCgroup); + } + + writeFile(cgroupFile, *cgroup); + } + } + + // Kill any processes left in the cgroup or build user. + DerivationBuilderImpl::prepareUser(); + } + + void prepareSandbox() override + { + /* Create a temporary directory in which we set up the chroot + environment using bind-mounts. We put it in the Nix store + so that the build outputs can be moved efficiently from the + chroot to their final location. */ + auto chrootParentDir = store.Store::toRealPath(drvPath) + ".chroot"; + deletePath(chrootParentDir); + + /* Clean up the chroot directory automatically. */ + autoDelChroot = std::make_shared(chrootParentDir); + + printMsg(lvlChatty, "setting up chroot environment in '%1%'", chrootParentDir); + + if (mkdir(chrootParentDir.c_str(), 0700) == -1) + throw SysError("cannot create '%s'", chrootRootDir); + + chrootRootDir = chrootParentDir + "/root"; + + if (mkdir(chrootRootDir.c_str(), buildUser && buildUser->getUIDCount() != 1 ? 0755 : 0750) == -1) + throw SysError("cannot create '%1%'", chrootRootDir); + + if (buildUser + && chown( + chrootRootDir.c_str(), buildUser->getUIDCount() != 1 ? buildUser->getUID() : 0, buildUser->getGID()) + == -1) + throw SysError("cannot change ownership of '%1%'", chrootRootDir); + + /* Create a writable /tmp in the chroot. Many builders need + this. (Of course they should really respect $TMPDIR + instead.) */ + Path chrootTmpDir = chrootRootDir + "/tmp"; + createDirs(chrootTmpDir); + chmod_(chrootTmpDir, 01777); + + /* Create a /etc/passwd with entries for the build user and the + nobody account. The latter is kind of a hack to support + Samba-in-QEMU. */ + createDirs(chrootRootDir + "/etc"); + if (drvOptions.useUidRange(drv)) + chownToBuilder(chrootRootDir + "/etc"); + + if (drvOptions.useUidRange(drv) && (!buildUser || buildUser->getUIDCount() < 65536)) + throw Error("feature 'uid-range' requires the setting '%s' to be enabled", settings.autoAllocateUids.name); + + /* Declare the build user's group so that programs get a consistent + view of the system (e.g., "id -gn"). */ + writeFile( + chrootRootDir + "/etc/group", + fmt("root:x:0:\n" + "nixbld:!:%1%:\n" + "nogroup:x:65534:\n", + sandboxGid())); + + /* Create /etc/hosts with localhost entry. */ + if (derivationType.isSandboxed()) + writeFile(chrootRootDir + "/etc/hosts", "127.0.0.1 localhost\n::1 localhost\n"); + + /* Make the closure of the inputs available in the chroot, + rather than the whole Nix store. This prevents any access + to undeclared dependencies. Directories are bind-mounted, + while other inputs are hard-linked (since only directories + can be bind-mounted). !!! As an extra security + precaution, make the fake Nix store only writable by the + build user. */ + Path chrootStoreDir = chrootRootDir + store.storeDir; + createDirs(chrootStoreDir); + chmod_(chrootStoreDir, 01775); + + if (buildUser && chown(chrootStoreDir.c_str(), 0, buildUser->getGID()) == -1) + throw SysError("cannot change ownership of '%1%'", chrootStoreDir); + + pathsInChroot = getPathsInSandbox(); + + for (auto & i : inputPaths) { + auto p = store.printStorePath(i); + pathsInChroot.insert_or_assign(p, store.toRealPath(p)); + } + + /* If we're repairing, checking or rebuilding part of a + multiple-outputs derivation, it's possible that we're + rebuilding a path that is in settings.sandbox-paths + (typically the dependencies of /bin/sh). Throw them + out. */ + for (auto & i : drv.outputsAndOptPaths(store)) { + /* If the name isn't known a priori (i.e. floating + content-addressing derivation), the temporary location we use + should be fresh. Freshness means it is impossible that the path + is already in the sandbox, so we don't need to worry about + removing it. */ + if (i.second.second) + pathsInChroot.erase(store.printStorePath(*i.second.second)); + } + + if (cgroup) { + if (mkdir(cgroup->c_str(), 0755) != 0) + throw SysError("creating cgroup '%s'", *cgroup); + chownToBuilder(*cgroup); + chownToBuilder(*cgroup + "/cgroup.procs"); + chownToBuilder(*cgroup + "/cgroup.threads"); + // chownToBuilder(*cgroup + "/cgroup.subtree_control"); + } + } + + Strings getPreBuildHookArgs() override + { + assert(!chrootRootDir.empty()); + return Strings({store.printStorePath(drvPath), chrootRootDir}); + } + + Path realPathInSandbox(const Path & p) override + { + // FIXME: why the needsHashRewrite() conditional? + return !needsHashRewrite() ? chrootRootDir + p : store.toRealPath(p); + } + + void startChild() override + { + /* Set up private namespaces for the build: + + - The PID namespace causes the build to start as PID 1. + Processes outside of the chroot are not visible to those + on the inside, but processes inside the chroot are + visible from the outside (though with different PIDs). + + - The private mount namespace ensures that all the bind + mounts we do will only show up in this process and its + children, and will disappear automatically when we're + done. + + - The private network namespace ensures that the builder + cannot talk to the outside world (or vice versa). It + only has a private loopback interface. (Fixed-output + derivations are not run in a private network namespace + to allow functions like fetchurl to work.) + + - The IPC namespace prevents the builder from communicating + with outside processes using SysV IPC mechanisms (shared + memory, message queues, semaphores). It also ensures + that all IPC objects are destroyed when the builder + exits. + + - The UTS namespace ensures that builders see a hostname of + localhost rather than the actual hostname. + + We use a helper process to do the clone() to work around + clone() being broken in multi-threaded programs due to + at-fork handlers not being run. Note that we use + CLONE_PARENT to ensure that the real builder is parented to + us. + */ + + userNamespaceSync.create(); + + usingUserNamespace = userNamespacesSupported(); + + Pipe sendPid; + sendPid.create(); + + Pid helper = startProcess([&]() { + sendPid.readSide.close(); + + /* We need to open the slave early, before + CLONE_NEWUSER. Otherwise we get EPERM when running as + root. */ + openSlave(); + + try { + /* Drop additional groups here because we can't do it + after we've created the new user namespace. */ + if (setgroups(0, 0) == -1) { + if (errno != EPERM) + throw SysError("setgroups failed"); + if (settings.requireDropSupplementaryGroups) + throw Error( + "setgroups failed. Set the require-drop-supplementary-groups option to false to skip this step."); + } + + ProcessOptions options; + options.cloneFlags = CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWUTS | CLONE_PARENT | SIGCHLD; + if (derivationType.isSandboxed()) + options.cloneFlags |= CLONE_NEWNET; + if (usingUserNamespace) + options.cloneFlags |= CLONE_NEWUSER; + + pid_t child = startProcess([&]() { runChild(); }, options); + + writeFull(sendPid.writeSide.get(), fmt("%d\n", child)); + _exit(0); + } catch (...) { + handleChildException(true); + _exit(1); + } + }); + + sendPid.writeSide.close(); + + if (helper.wait() != 0) { + processSandboxSetupMessages(); + // Only reached if the child process didn't send an exception. + throw Error("unable to start build process"); + } + + userNamespaceSync.readSide = -1; + + /* Close the write side to prevent runChild() from hanging + reading from this. */ + Finally cleanup([&]() { userNamespaceSync.writeSide = -1; }); + + auto ss = tokenizeString>(readLine(sendPid.readSide.get())); + assert(ss.size() == 1); + pid = string2Int(ss[0]).value(); + + if (usingUserNamespace) { + /* Set the UID/GID mapping of the builder's user namespace + such that the sandbox user maps to the build user, or to + the calling user (if build users are disabled). */ + uid_t hostUid = buildUser ? buildUser->getUID() : getuid(); + uid_t hostGid = buildUser ? buildUser->getGID() : getgid(); + uid_t nrIds = buildUser ? buildUser->getUIDCount() : 1; + + writeFile("/proc/" + std::to_string(pid) + "/uid_map", fmt("%d %d %d", sandboxUid(), hostUid, nrIds)); + + if (!buildUser || buildUser->getUIDCount() == 1) + writeFile("/proc/" + std::to_string(pid) + "/setgroups", "deny"); + + writeFile("/proc/" + std::to_string(pid) + "/gid_map", fmt("%d %d %d", sandboxGid(), hostGid, nrIds)); + } else { + debug("note: not using a user namespace"); + if (!buildUser) + throw Error( + "cannot perform a sandboxed build because user namespaces are not enabled; check /proc/sys/user/max_user_namespaces"); + } + + /* Now that we now the sandbox uid, we can write + /etc/passwd. */ + writeFile( + chrootRootDir + "/etc/passwd", + fmt("root:x:0:0:Nix build user:%3%:/noshell\n" + "nixbld:x:%1%:%2%:Nix build user:%3%:/noshell\n" + "nobody:x:65534:65534:Nobody:/:/noshell\n", + sandboxUid(), + sandboxGid(), + settings.sandboxBuildDir)); + + /* Save the mount- and user namespace of the child. We have to do this + *before* the child does a chroot. */ + sandboxMountNamespace = open(fmt("/proc/%d/ns/mnt", (pid_t) pid).c_str(), O_RDONLY); + if (sandboxMountNamespace.get() == -1) + throw SysError("getting sandbox mount namespace"); + + if (usingUserNamespace) { + sandboxUserNamespace = open(fmt("/proc/%d/ns/user", (pid_t) pid).c_str(), O_RDONLY); + if (sandboxUserNamespace.get() == -1) + throw SysError("getting sandbox user namespace"); + } + + /* Move the child into its own cgroup. */ + if (cgroup) + writeFile(*cgroup + "/cgroup.procs", fmt("%d", (pid_t) pid)); + + /* Signal the builder that we've updated its user namespace. */ + writeFull(userNamespaceSync.writeSide.get(), "1"); + } + + void enterChroot() override + { + userNamespaceSync.writeSide = -1; + + if (drainFD(userNamespaceSync.readSide.get()) != "1") + throw Error("user namespace initialisation failed"); + + userNamespaceSync.readSide = -1; + + if (derivationType.isSandboxed()) { + + /* Initialise the loopback interface. */ + AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP)); + if (!fd) + throw SysError("cannot open IP socket"); + + struct ifreq ifr; + strcpy(ifr.ifr_name, "lo"); + ifr.ifr_flags = IFF_UP | IFF_LOOPBACK | IFF_RUNNING; + if (ioctl(fd.get(), SIOCSIFFLAGS, &ifr) == -1) + throw SysError("cannot set loopback interface flags"); + } + + /* Set the hostname etc. to fixed values. */ + char hostname[] = "localhost"; + if (sethostname(hostname, sizeof(hostname)) == -1) + throw SysError("cannot set host name"); + char domainname[] = "(none)"; // kernel default + if (setdomainname(domainname, sizeof(domainname)) == -1) + throw SysError("cannot set domain name"); + + /* Make all filesystems private. This is necessary + because subtrees may have been mounted as "shared" + (MS_SHARED). (Systemd does this, for instance.) Even + though we have a private mount namespace, mounting + filesystems on top of a shared subtree still propagates + outside of the namespace. Making a subtree private is + local to the namespace, though, so setting MS_PRIVATE + does not affect the outside world. */ + if (mount(0, "/", 0, MS_PRIVATE | MS_REC, 0) == -1) + throw SysError("unable to make '/' private"); + + /* Bind-mount chroot directory to itself, to treat it as a + different filesystem from /, as needed for pivot_root. */ + if (mount(chrootRootDir.c_str(), chrootRootDir.c_str(), 0, MS_BIND, 0) == -1) + throw SysError("unable to bind mount '%1%'", chrootRootDir); + + /* Bind-mount the sandbox's Nix store onto itself so that + we can mark it as a "shared" subtree, allowing bind + mounts made in *this* mount namespace to be propagated + into the child namespace created by the + unshare(CLONE_NEWNS) call below. + + Marking chrootRootDir as MS_SHARED causes pivot_root() + to fail with EINVAL. Don't know why. */ + Path chrootStoreDir = chrootRootDir + store.storeDir; + + if (mount(chrootStoreDir.c_str(), chrootStoreDir.c_str(), 0, MS_BIND, 0) == -1) + throw SysError("unable to bind mount the Nix store", chrootStoreDir); + + if (mount(0, chrootStoreDir.c_str(), 0, MS_SHARED, 0) == -1) + throw SysError("unable to make '%s' shared", chrootStoreDir); + + /* Set up a nearly empty /dev, unless the user asked to + bind-mount the host /dev. */ + Strings ss; + if (pathsInChroot.find("/dev") == pathsInChroot.end()) { + createDirs(chrootRootDir + "/dev/shm"); + createDirs(chrootRootDir + "/dev/pts"); + ss.push_back("/dev/full"); + if (store.config.systemFeatures.get().count("kvm") && pathExists("/dev/kvm")) + ss.push_back("/dev/kvm"); + ss.push_back("/dev/null"); + ss.push_back("/dev/random"); + ss.push_back("/dev/tty"); + ss.push_back("/dev/urandom"); + ss.push_back("/dev/zero"); + createSymlink("/proc/self/fd", chrootRootDir + "/dev/fd"); + createSymlink("/proc/self/fd/0", chrootRootDir + "/dev/stdin"); + createSymlink("/proc/self/fd/1", chrootRootDir + "/dev/stdout"); + createSymlink("/proc/self/fd/2", chrootRootDir + "/dev/stderr"); + } + + /* Fixed-output derivations typically need to access the + network, so give them access to /etc/resolv.conf and so + on. */ + if (!derivationType.isSandboxed()) { + // Only use nss functions to resolve hosts and + // services. Don’t use it for anything else that may + // be configured for this system. This limits the + // potential impurities introduced in fixed-outputs. + writeFile(chrootRootDir + "/etc/nsswitch.conf", "hosts: files dns\nservices: files\n"); + + /* N.B. it is realistic that these paths might not exist. It + happens when testing Nix building fixed-output derivations + within a pure derivation. */ + for (auto & path : {"/etc/resolv.conf", "/etc/services", "/etc/hosts"}) + if (pathExists(path)) + ss.push_back(path); + + if (settings.caFile != "") { + Path caFile = settings.caFile; + if (pathExists(caFile)) + pathsInChroot.try_emplace("/etc/ssl/certs/ca-certificates.crt", canonPath(caFile, true), true); + } + } + + for (auto & i : ss) { + // For backwards-compatibility, resolve all the symlinks in the + // chroot paths. + auto canonicalPath = canonPath(i, true); + pathsInChroot.emplace(i, canonicalPath); + } + + /* Bind-mount all the directories from the "host" + filesystem that we want in the chroot + environment. */ + for (auto & i : pathsInChroot) { + if (i.second.source == "/proc") + continue; // backwards compatibility + +# if HAVE_EMBEDDED_SANDBOX_SHELL + if (i.second.source == "__embedded_sandbox_shell__") { + static unsigned char sh[] = { +# include "embedded-sandbox-shell.gen.hh" + }; + auto dst = chrootRootDir + i.first; + createDirs(dirOf(dst)); + writeFile(dst, std::string_view((const char *) sh, sizeof(sh))); + chmod_(dst, 0555); + } else +# endif + { + doBind(i.second.source, chrootRootDir + i.first, i.second.optional); + } + } + + /* Bind a new instance of procfs on /proc. */ + createDirs(chrootRootDir + "/proc"); + if (mount("none", (chrootRootDir + "/proc").c_str(), "proc", 0, 0) == -1) + throw SysError("mounting /proc"); + + /* Mount sysfs on /sys. */ + if (buildUser && buildUser->getUIDCount() != 1) { + createDirs(chrootRootDir + "/sys"); + if (mount("none", (chrootRootDir + "/sys").c_str(), "sysfs", 0, 0) == -1) + throw SysError("mounting /sys"); + } + + /* Mount a new tmpfs on /dev/shm to ensure that whatever + the builder puts in /dev/shm is cleaned up automatically. */ + if (pathExists("/dev/shm") + && mount( + "none", + (chrootRootDir + "/dev/shm").c_str(), + "tmpfs", + 0, + fmt("size=%s", settings.sandboxShmSize).c_str()) + == -1) + throw SysError("mounting /dev/shm"); + + /* Mount a new devpts on /dev/pts. Note that this + requires the kernel to be compiled with + CONFIG_DEVPTS_MULTIPLE_INSTANCES=y (which is the case + if /dev/ptx/ptmx exists). */ + if (pathExists("/dev/pts/ptmx") && !pathExists(chrootRootDir + "/dev/ptmx") + && !pathsInChroot.count("/dev/pts")) { + if (mount("none", (chrootRootDir + "/dev/pts").c_str(), "devpts", 0, "newinstance,mode=0620") == 0) { + createSymlink("/dev/pts/ptmx", chrootRootDir + "/dev/ptmx"); + + /* Make sure /dev/pts/ptmx is world-writable. With some + Linux versions, it is created with permissions 0. */ + chmod_(chrootRootDir + "/dev/pts/ptmx", 0666); + } else { + if (errno != EINVAL) + throw SysError("mounting /dev/pts"); + doBind("/dev/pts", chrootRootDir + "/dev/pts"); + doBind("/dev/ptmx", chrootRootDir + "/dev/ptmx"); + } + } + + /* Make /etc unwritable */ + if (!drvOptions.useUidRange(drv)) + chmod_(chrootRootDir + "/etc", 0555); + + /* Unshare this mount namespace. This is necessary because + pivot_root() below changes the root of the mount + namespace. This means that the call to setns() in + addDependency() would hide the host's filesystem, + making it impossible to bind-mount paths from the host + Nix store into the sandbox. Therefore, we save the + pre-pivot_root namespace in + sandboxMountNamespace. Since we made /nix/store a + shared subtree above, this allows addDependency() to + make paths appear in the sandbox. */ + if (unshare(CLONE_NEWNS) == -1) + throw SysError("unsharing mount namespace"); + + /* Unshare the cgroup namespace. This means + /proc/self/cgroup will show the child's cgroup as '/' + rather than whatever it is in the parent. */ + if (cgroup && unshare(CLONE_NEWCGROUP) == -1) + throw SysError("unsharing cgroup namespace"); + + /* Do the chroot(). */ + if (chdir(chrootRootDir.c_str()) == -1) + throw SysError("cannot change directory to '%1%'", chrootRootDir); + + if (mkdir("real-root", 0500) == -1) + throw SysError("cannot create real-root directory"); + + if (pivot_root(".", "real-root") == -1) + throw SysError("cannot pivot old root directory onto '%1%'", (chrootRootDir + "/real-root")); + + if (chroot(".") == -1) + throw SysError("cannot change root directory to '%1%'", chrootRootDir); + + if (umount2("real-root", MNT_DETACH) == -1) + throw SysError("cannot unmount real root filesystem"); + + if (rmdir("real-root") == -1) + throw SysError("cannot remove real-root directory"); + + LinuxDerivationBuilder::enterChroot(); + } + + void setUser() override + { + /* Switch to the sandbox uid/gid in the user namespace, + which corresponds to the build user or calling user in + the parent namespace. */ + if (setgid(sandboxGid()) == -1) + throw SysError("setgid failed"); + if (setuid(sandboxUid()) == -1) + throw SysError("setuid failed"); + } + + std::variant, SingleDrvOutputs> unprepareBuild() override + { + sandboxMountNamespace = -1; + sandboxUserNamespace = -1; + + return DerivationBuilderImpl::unprepareBuild(); + } + + void killSandbox(bool getStats) override + { + if (cgroup) { + auto stats = destroyCgroup(*cgroup); + if (getStats) { + buildResult.cpuUser = stats.cpuUser; + buildResult.cpuSystem = stats.cpuSystem; + } + return; + } + + DerivationBuilderImpl::killSandbox(getStats); + } + + void cleanupBuild() override + { + DerivationBuilderImpl::cleanupBuild(); + + /* Move paths out of the chroot for easier debugging of + build failures. */ + if (buildMode == bmNormal) + for (auto & [_, status] : initialOutputs) { + if (!status.known) + continue; + if (buildMode != bmCheck && status.known->isValid()) + continue; + auto p = store.toRealPath(status.known->path); + if (pathExists(chrootRootDir + p)) + std::filesystem::rename((chrootRootDir + p), p); + } + } + + void addDependency(const StorePath & path) override + { + if (isAllowed(path)) + return; + + addedPaths.insert(path); + + debug("materialising '%s' in the sandbox", store.printStorePath(path)); + + Path source = store.Store::toRealPath(path); + Path target = chrootRootDir + store.printStorePath(path); + + if (pathExists(target)) { + // There is a similar debug message in doBind, so only run it in this block to not have double messages. + debug("bind-mounting %s -> %s", target, source); + throw Error("store path '%s' already exists in the sandbox", store.printStorePath(path)); + } + + /* Bind-mount the path into the sandbox. This requires + entering its mount namespace, which is not possible + in multithreaded programs. So we do this in a + child process.*/ + Pid child(startProcess([&]() { + if (usingUserNamespace && (setns(sandboxUserNamespace.get(), 0) == -1)) + throw SysError("entering sandbox user namespace"); + + if (setns(sandboxMountNamespace.get(), 0) == -1) + throw SysError("entering sandbox mount namespace"); + + doBind(source, target); + + _exit(0); + })); + + int status = child.wait(); + if (status != 0) + throw Error("could not add path '%s' to sandbox", store.printStorePath(path)); + } +}; + +} + +#endif diff --git a/src/libstore/unix/build/local-derivation-goal.cc b/src/libstore/unix/build/local-derivation-goal.cc deleted file mode 100644 index 06a2f85be84..00000000000 --- a/src/libstore/unix/build/local-derivation-goal.cc +++ /dev/null @@ -1,3084 +0,0 @@ -#include "local-derivation-goal.hh" -#include "indirect-root-store.hh" -#include "hook-instance.hh" -#include "worker.hh" -#include "builtins.hh" -#include "builtins/buildenv.hh" -#include "path-references.hh" -#include "finally.hh" -#include "util.hh" -#include "archive.hh" -#include "git.hh" -#include "compression.hh" -#include "daemon.hh" -#include "topo-sort.hh" -#include "callback.hh" -#include "json-utils.hh" -#include "current-process.hh" -#include "child.hh" -#include "unix-domain-socket.hh" -#include "posix-fs-canonicalise.hh" -#include "posix-source-accessor.hh" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#if HAVE_STATVFS -#include -#endif - -/* Includes required for chroot support. */ -#if __linux__ -# include "fchmodat2-compat.hh" -# include -# include -# include -# include -# include -# include -# include -# include -# include "namespaces.hh" -# if HAVE_SECCOMP -# include -# endif -# define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root, put_old)) -# include "cgroup.hh" -# include "personality.hh" -#endif - -#if __APPLE__ -#include -#include -#include - -/* This definition is undocumented but depended upon by all major browsers. */ -extern "C" int sandbox_init_with_parameters(const char *profile, uint64_t flags, const char *const parameters[], char **errorbuf); -#endif - -#include -#include -#include - -#include "strings.hh" -#include "signals.hh" - -namespace nix { - -void handleDiffHook( - uid_t uid, uid_t gid, - const Path & tryA, const Path & tryB, - const Path & drvPath, const Path & tmpDir) -{ - auto & diffHookOpt = settings.diffHook.get(); - if (diffHookOpt && settings.runDiffHook) { - auto & diffHook = *diffHookOpt; - try { - auto diffRes = runProgram(RunOptions { - .program = diffHook, - .lookupPath = true, - .args = {tryA, tryB, drvPath, tmpDir}, - .uid = uid, - .gid = gid, - .chdir = "/" - }); - if (!statusOk(diffRes.first)) - throw ExecError(diffRes.first, - "diff-hook program '%1%' %2%", - diffHook, - statusToString(diffRes.first)); - - if (diffRes.second != "") - printError(chomp(diffRes.second)); - } catch (Error & error) { - ErrorInfo ei = error.info(); - // FIXME: wrap errors. - ei.msg = HintFmt("diff hook execution failed: %s", ei.msg.str()); - logError(ei); - } - } -} - -const Path LocalDerivationGoal::homeDir = "/homeless-shelter"; - - -LocalDerivationGoal::~LocalDerivationGoal() -{ - /* Careful: we should never ever throw an exception from a - destructor. */ - try { deleteTmpDir(false); } catch (...) { ignoreExceptionInDestructor(); } - try { killChild(); } catch (...) { ignoreExceptionInDestructor(); } - try { stopDaemon(); } catch (...) { ignoreExceptionInDestructor(); } -} - - -inline bool LocalDerivationGoal::needsHashRewrite() -{ -#if __linux__ - return !useChroot; -#else - /* Darwin requires hash rewriting even when sandboxing is enabled. */ - return true; -#endif -} - - -LocalStore & LocalDerivationGoal::getLocalStore() -{ - auto p = dynamic_cast(&worker.store); - assert(p); - return *p; -} - - -void LocalDerivationGoal::killChild() -{ - if (pid != -1) { - worker.childTerminated(this); - - /* If we're using a build user, then there is a tricky race - condition: if we kill the build user before the child has - done its setuid() to the build user uid, then it won't be - killed, and we'll potentially lock up in pid.wait(). So - also send a conventional kill to the child. */ - ::kill(-pid, SIGKILL); /* ignore the result */ - - killSandbox(true); - - pid.wait(); - } - - DerivationGoal::killChild(); -} - - -void LocalDerivationGoal::killSandbox(bool getStats) -{ - if (cgroup) { - #if __linux__ - auto stats = destroyCgroup(*cgroup); - if (getStats) { - buildResult.cpuUser = stats.cpuUser; - buildResult.cpuSystem = stats.cpuSystem; - } - #else - unreachable(); - #endif - } - - else if (buildUser) { - auto uid = buildUser->getUID(); - assert(uid != 0); - killUser(uid); - } -} - - -Goal::Co LocalDerivationGoal::tryLocalBuild() -{ -#if __APPLE__ - additionalSandboxProfile = parsedDrv->getStringAttr("__sandboxProfile").value_or(""); -#endif - - unsigned int curBuilds = worker.getNrLocalBuilds(); - if (curBuilds >= settings.maxBuildJobs) { - worker.waitForBuildSlot(shared_from_this()); - outputLocks.unlock(); - co_await Suspend{}; - co_return tryToBuild(); - } - - assert(derivationType); - - /* Are we doing a chroot build? */ - { - auto noChroot = parsedDrv->getBoolAttr("__noChroot"); - if (settings.sandboxMode == smEnabled) { - if (noChroot) - throw Error("derivation '%s' has '__noChroot' set, " - "but that's not allowed when 'sandbox' is 'true'", worker.store.printStorePath(drvPath)); -#if __APPLE__ - if (additionalSandboxProfile != "") - throw Error("derivation '%s' specifies a sandbox profile, " - "but this is only allowed when 'sandbox' is 'relaxed'", worker.store.printStorePath(drvPath)); -#endif - useChroot = true; - } - else if (settings.sandboxMode == smDisabled) - useChroot = false; - else if (settings.sandboxMode == smRelaxed) - useChroot = derivationType->isSandboxed() && !noChroot; - } - - auto & localStore = getLocalStore(); - if (localStore.storeDir != localStore.realStoreDir.get()) { - #if __linux__ - useChroot = true; - #else - throw Error("building using a diverted store is not supported on this platform"); - #endif - } - - #if __linux__ - if (useChroot) { - if (!mountAndPidNamespacesSupported()) { - if (!settings.sandboxFallback) - throw Error("this system does not support the kernel namespaces that are required for sandboxing; use '--no-sandbox' to disable sandboxing"); - debug("auto-disabling sandboxing because the prerequisite namespaces are not available"); - useChroot = false; - } - } - #endif - - if (useBuildUsers()) { - if (!buildUser) - buildUser = acquireUserLock(parsedDrv->useUidRange() ? 65536 : 1, useChroot); - - if (!buildUser) { - if (!actLock) - actLock = std::make_unique(*logger, lvlWarn, actBuildWaiting, - fmt("waiting for a free build user ID for '%s'", Magenta(worker.store.printStorePath(drvPath)))); - worker.waitForAWhile(shared_from_this()); - co_await Suspend{}; - co_return tryLocalBuild(); - } - } - - actLock.reset(); - - try { - - /* Okay, we have to build. */ - startBuilder(); - - } catch (BuildError & e) { - outputLocks.unlock(); - buildUser.reset(); - worker.permanentFailure = true; - co_return done(BuildResult::InputRejected, {}, std::move(e)); - } - - started(); - co_await Suspend{}; - // after EOF on child - co_return buildDone(); -} - -static void chmod_(const Path & path, mode_t mode) -{ - if (chmod(path.c_str(), mode) == -1) - throw SysError("setting permissions on '%s'", path); -} - - -/* Move/rename path 'src' to 'dst'. Temporarily make 'src' writable if - it's a directory and we're not root (to be able to update the - directory's parent link ".."). */ -static void movePath(const Path & src, const Path & dst) -{ - auto st = lstat(src); - - bool changePerm = (geteuid() && S_ISDIR(st.st_mode) && !(st.st_mode & S_IWUSR)); - - if (changePerm) - chmod_(src, st.st_mode | S_IWUSR); - - std::filesystem::rename(src, dst); - - if (changePerm) - chmod_(dst, st.st_mode); -} - - -extern void replaceValidPath(const Path & storePath, const Path & tmpPath); - - -int LocalDerivationGoal::getChildStatus() -{ - return hook ? DerivationGoal::getChildStatus() : pid.kill(); -} - -void LocalDerivationGoal::closeReadPipes() -{ - if (hook) { - DerivationGoal::closeReadPipes(); - } else - builderOut.close(); -} - - -void LocalDerivationGoal::cleanupHookFinally() -{ - /* Release the build user at the end of this function. We don't do - it right away because we don't want another build grabbing this - uid and then messing around with our output. */ - buildUser.reset(); -} - - -void LocalDerivationGoal::cleanupPreChildKill() -{ - sandboxMountNamespace = -1; - sandboxUserNamespace = -1; -} - - -void LocalDerivationGoal::cleanupPostChildKill() -{ - /* When running under a build user, make sure that all processes - running under that uid are gone. This is to prevent a - malicious user from leaving behind a process that keeps files - open and modifies them after they have been chown'ed to - root. */ - killSandbox(true); - - /* Terminate the recursive Nix daemon. */ - stopDaemon(); -} - - -bool LocalDerivationGoal::cleanupDecideWhetherDiskFull() -{ - bool diskFull = false; - - /* Heuristically check whether the build failure may have - been caused by a disk full condition. We have no way - of knowing whether the build actually got an ENOSPC. - So instead, check if the disk is (nearly) full now. If - so, we don't mark this build as a permanent failure. */ -#if HAVE_STATVFS - { - auto & localStore = getLocalStore(); - uint64_t required = 8ULL * 1024 * 1024; // FIXME: make configurable - struct statvfs st; - if (statvfs(localStore.realStoreDir.get().c_str(), &st) == 0 && - (uint64_t) st.f_bavail * st.f_bsize < required) - diskFull = true; - if (statvfs(tmpDir.c_str(), &st) == 0 && - (uint64_t) st.f_bavail * st.f_bsize < required) - diskFull = true; - } -#endif - - deleteTmpDir(false); - - /* Move paths out of the chroot for easier debugging of - build failures. */ - if (useChroot && buildMode == bmNormal) - for (auto & [_, status] : initialOutputs) { - if (!status.known) continue; - if (buildMode != bmCheck && status.known->isValid()) continue; - auto p = worker.store.toRealPath(status.known->path); - if (pathExists(chrootRootDir + p)) - std::filesystem::rename((chrootRootDir + p), p); - } - - return diskFull; -} - - -void LocalDerivationGoal::cleanupPostOutputsRegisteredModeCheck() -{ - deleteTmpDir(true); -} - - -void LocalDerivationGoal::cleanupPostOutputsRegisteredModeNonCheck() -{ - /* Delete unused redirected outputs (when doing hash rewriting). */ - for (auto & i : redirectedOutputs) - deletePath(worker.store.Store::toRealPath(i.second)); - - /* Delete the chroot (if we were using one). */ - autoDelChroot.reset(); /* this runs the destructor */ - - cleanupPostOutputsRegisteredModeCheck(); -} - -#if __linux__ -static void doBind(const Path & source, const Path & target, bool optional = false) { - debug("bind mounting '%1%' to '%2%'", source, target); - - auto bindMount = [&]() { - if (mount(source.c_str(), target.c_str(), "", MS_BIND | MS_REC, 0) == -1) - throw SysError("bind mount from '%1%' to '%2%' failed", source, target); - }; - - auto maybeSt = maybeLstat(source); - if (!maybeSt) { - if (optional) - return; - else - throw SysError("getting attributes of path '%1%'", source); - } - auto st = *maybeSt; - - if (S_ISDIR(st.st_mode)) { - createDirs(target); - bindMount(); - } else if (S_ISLNK(st.st_mode)) { - // Symlinks can (apparently) not be bind-mounted, so just copy it - createDirs(dirOf(target)); - copyFile( - std::filesystem::path(source), - std::filesystem::path(target), false); - } else { - createDirs(dirOf(target)); - writeFile(target, ""); - bindMount(); - } -}; -#endif - -/** - * Rethrow the current exception as a subclass of `Error`. - */ -static void rethrowExceptionAsError() -{ - try { - throw; - } catch (Error &) { - throw; - } catch (std::exception & e) { - throw Error(e.what()); - } catch (...) { - throw Error("unknown exception"); - } -} - -/** - * Send the current exception to the parent in the format expected by - * `LocalDerivationGoal::processSandboxSetupMessages()`. - */ -static void handleChildException(bool sendException) -{ - try { - rethrowExceptionAsError(); - } catch (Error & e) { - if (sendException) { - writeFull(STDERR_FILENO, "\1\n"); - FdSink sink(STDERR_FILENO); - sink << e; - sink.flush(); - } else - std::cerr << e.msg(); - } -} - -void LocalDerivationGoal::startBuilder() -{ - if ((buildUser && buildUser->getUIDCount() != 1) - #if __linux__ - || settings.useCgroups - #endif - ) - { - #if __linux__ - experimentalFeatureSettings.require(Xp::Cgroups); - - /* If we're running from the daemon, then this will return the - root cgroup of the service. Otherwise, it will return the - current cgroup. */ - auto rootCgroup = getRootCgroup(); - auto cgroupFS = getCgroupFS(); - if (!cgroupFS) - throw Error("cannot determine the cgroups file system"); - auto rootCgroupPath = canonPath(*cgroupFS + "/" + rootCgroup); - if (!pathExists(rootCgroupPath)) - throw Error("expected cgroup directory '%s'", rootCgroupPath); - - static std::atomic counter{0}; - - cgroup = buildUser - ? fmt("%s/nix-build-uid-%d", rootCgroupPath, buildUser->getUID()) - : fmt("%s/nix-build-pid-%d-%d", rootCgroupPath, getpid(), counter++); - - debug("using cgroup '%s'", *cgroup); - - /* When using a build user, record the cgroup we used for that - user so that if we got interrupted previously, we can kill - any left-over cgroup first. */ - if (buildUser) { - auto cgroupsDir = settings.nixStateDir + "/cgroups"; - createDirs(cgroupsDir); - - auto cgroupFile = fmt("%s/%d", cgroupsDir, buildUser->getUID()); - - if (pathExists(cgroupFile)) { - auto prevCgroup = readFile(cgroupFile); - destroyCgroup(prevCgroup); - } - - writeFile(cgroupFile, *cgroup); - } - - #else - throw Error("cgroups are not supported on this platform"); - #endif - } - - /* Make sure that no other processes are executing under the - sandbox uids. This must be done before any chownToBuilder() - calls. */ - killSandbox(false); - - /* Right platform? */ - if (!parsedDrv->canBuildLocally(worker.store)) - throw Error("a '%s' with features {%s} is required to build '%s', but I am a '%s' with features {%s}", - drv->platform, - concatStringsSep(", ", parsedDrv->getRequiredSystemFeatures()), - worker.store.printStorePath(drvPath), - settings.thisSystem, - concatStringsSep(", ", worker.store.systemFeatures)); - - /* Create a temporary directory where the build will take - place. */ - topTmpDir = createTempDir(settings.buildDir.get().value_or(""), "nix-build-" + std::string(drvPath.name()), false, false, 0700); -#if __APPLE__ - if (false) { -#else - if (useChroot) { -#endif - /* If sandboxing is enabled, put the actual TMPDIR underneath - an inaccessible root-owned directory, to prevent outside - access. - - On macOS, we don't use an actual chroot, so this isn't - possible. Any mitigation along these lines would have to be - done directly in the sandbox profile. */ - tmpDir = topTmpDir + "/build"; - createDir(tmpDir, 0700); - } else { - tmpDir = topTmpDir; - } - chownToBuilder(tmpDir); - - for (auto & [outputName, status] : initialOutputs) { - /* Set scratch path we'll actually use during the build. - - If we're not doing a chroot build, but we have some valid - output paths. Since we can't just overwrite or delete - them, we have to do hash rewriting: i.e. in the - environment/arguments passed to the build, we replace the - hashes of the valid outputs with unique dummy strings; - after the build, we discard the redirected outputs - corresponding to the valid outputs, and rewrite the - contents of the new outputs to replace the dummy strings - with the actual hashes. */ - auto scratchPath = - !status.known - ? makeFallbackPath(outputName) - : !needsHashRewrite() - /* Can always use original path in sandbox */ - ? status.known->path - : !status.known->isPresent() - /* If path doesn't yet exist can just use it */ - ? status.known->path - : buildMode != bmRepair && !status.known->isValid() - /* If we aren't repairing we'll delete a corrupted path, so we - can use original path */ - ? status.known->path - : /* If we are repairing or the path is totally valid, we'll need - to use a temporary path */ - makeFallbackPath(status.known->path); - scratchOutputs.insert_or_assign(outputName, scratchPath); - - /* Substitute output placeholders with the scratch output paths. - We'll use during the build. */ - inputRewrites[hashPlaceholder(outputName)] = worker.store.printStorePath(scratchPath); - - /* Additional tasks if we know the final path a priori. */ - if (!status.known) continue; - auto fixedFinalPath = status.known->path; - - /* Additional tasks if the final and scratch are both known and - differ. */ - if (fixedFinalPath == scratchPath) continue; - - /* Ensure scratch path is ours to use. */ - deletePath(worker.store.printStorePath(scratchPath)); - - /* Rewrite and unrewrite paths */ - { - std::string h1 { fixedFinalPath.hashPart() }; - std::string h2 { scratchPath.hashPart() }; - inputRewrites[h1] = h2; - } - - redirectedOutputs.insert_or_assign(std::move(fixedFinalPath), std::move(scratchPath)); - } - - /* Construct the environment passed to the builder. */ - initEnv(); - - writeStructuredAttrs(); - - /* Handle exportReferencesGraph(), if set. */ - if (!parsedDrv->getStructuredAttrs()) { - /* The `exportReferencesGraph' feature allows the references graph - to be passed to a builder. This attribute should be a list of - pairs [name1 path1 name2 path2 ...]. The references graph of - each `pathN' will be stored in a text file `nameN' in the - temporary build directory. The text files have the format used - by `nix-store --register-validity'. However, the deriver - fields are left empty. */ - auto s = getOr(drv->env, "exportReferencesGraph", ""); - Strings ss = tokenizeString(s); - if (ss.size() % 2 != 0) - throw BuildError("odd number of tokens in 'exportReferencesGraph': '%1%'", s); - for (Strings::iterator i = ss.begin(); i != ss.end(); ) { - auto fileName = *i++; - static std::regex regex("[A-Za-z_][A-Za-z0-9_.-]*"); - if (!std::regex_match(fileName, regex)) - throw Error("invalid file name '%s' in 'exportReferencesGraph'", fileName); - - auto storePathS = *i++; - if (!worker.store.isInStore(storePathS)) - throw BuildError("'exportReferencesGraph' contains a non-store path '%1%'", storePathS); - auto storePath = worker.store.toStorePath(storePathS).first; - - /* Write closure info to . */ - writeFile(tmpDir + "/" + fileName, - worker.store.makeValidityRegistration( - worker.store.exportReferences({storePath}, inputPaths), false, false)); - } - } - - if (useChroot) { - - /* Allow a user-configurable set of directories from the - host file system. */ - pathsInChroot.clear(); - - for (auto i : settings.sandboxPaths.get()) { - if (i.empty()) continue; - bool optional = false; - if (i[i.size() - 1] == '?') { - optional = true; - i.pop_back(); - } - size_t p = i.find('='); - if (p == std::string::npos) - pathsInChroot[i] = {i, optional}; - else - pathsInChroot[i.substr(0, p)] = {i.substr(p + 1), optional}; - } - if (hasPrefix(worker.store.storeDir, tmpDirInSandbox)) - { - throw Error("`sandbox-build-dir` must not contain the storeDir"); - } - pathsInChroot[tmpDirInSandbox] = tmpDir; - - /* Add the closure of store paths to the chroot. */ - StorePathSet closure; - for (auto & i : pathsInChroot) - try { - if (worker.store.isInStore(i.second.source)) - worker.store.computeFSClosure(worker.store.toStorePath(i.second.source).first, closure); - } catch (InvalidPath & e) { - } catch (Error & e) { - e.addTrace({}, "while processing 'sandbox-paths'"); - throw; - } - for (auto & i : closure) { - auto p = worker.store.printStorePath(i); - pathsInChroot.insert_or_assign(p, p); - } - - PathSet allowedPaths = settings.allowedImpureHostPrefixes; - - /* This works like the above, except on a per-derivation level */ - auto impurePaths = parsedDrv->getStringsAttr("__impureHostDeps").value_or(Strings()); - - for (auto & i : impurePaths) { - bool found = false; - /* Note: we're not resolving symlinks here to prevent - giving a non-root user info about inaccessible - files. */ - Path canonI = canonPath(i); - /* If only we had a trie to do this more efficiently :) luckily, these are generally going to be pretty small */ - for (auto & a : allowedPaths) { - Path canonA = canonPath(a); - if (canonI == canonA || isInDir(canonI, canonA)) { - found = true; - break; - } - } - if (!found) - throw Error("derivation '%s' requested impure path '%s', but it was not in allowed-impure-host-deps", - worker.store.printStorePath(drvPath), i); - - /* Allow files in __impureHostDeps to be missing; e.g. - macOS 11+ has no /usr/lib/libSystem*.dylib */ - pathsInChroot[i] = {i, true}; - } - -#if __linux__ - /* Create a temporary directory in which we set up the chroot - environment using bind-mounts. We put it in the Nix store - so that the build outputs can be moved efficiently from the - chroot to their final location. */ - chrootParentDir = worker.store.Store::toRealPath(drvPath) + ".chroot"; - deletePath(chrootParentDir); - - /* Clean up the chroot directory automatically. */ - autoDelChroot = std::make_shared(chrootParentDir); - - printMsg(lvlChatty, "setting up chroot environment in '%1%'", chrootParentDir); - - if (mkdir(chrootParentDir.c_str(), 0700) == -1) - throw SysError("cannot create '%s'", chrootRootDir); - - chrootRootDir = chrootParentDir + "/root"; - - if (mkdir(chrootRootDir.c_str(), buildUser && buildUser->getUIDCount() != 1 ? 0755 : 0750) == -1) - throw SysError("cannot create '%1%'", chrootRootDir); - - if (buildUser && chown(chrootRootDir.c_str(), buildUser->getUIDCount() != 1 ? buildUser->getUID() : 0, buildUser->getGID()) == -1) - throw SysError("cannot change ownership of '%1%'", chrootRootDir); - - /* Create a writable /tmp in the chroot. Many builders need - this. (Of course they should really respect $TMPDIR - instead.) */ - Path chrootTmpDir = chrootRootDir + "/tmp"; - createDirs(chrootTmpDir); - chmod_(chrootTmpDir, 01777); - - /* Create a /etc/passwd with entries for the build user and the - nobody account. The latter is kind of a hack to support - Samba-in-QEMU. */ - createDirs(chrootRootDir + "/etc"); - if (parsedDrv->useUidRange()) - chownToBuilder(chrootRootDir + "/etc"); - - if (parsedDrv->useUidRange() && (!buildUser || buildUser->getUIDCount() < 65536)) - throw Error("feature 'uid-range' requires the setting '%s' to be enabled", settings.autoAllocateUids.name); - - /* Declare the build user's group so that programs get a consistent - view of the system (e.g., "id -gn"). */ - writeFile(chrootRootDir + "/etc/group", - fmt("root:x:0:\n" - "nixbld:!:%1%:\n" - "nogroup:x:65534:\n", sandboxGid())); - - /* Create /etc/hosts with localhost entry. */ - if (derivationType->isSandboxed()) - writeFile(chrootRootDir + "/etc/hosts", "127.0.0.1 localhost\n::1 localhost\n"); - - /* Make the closure of the inputs available in the chroot, - rather than the whole Nix store. This prevents any access - to undeclared dependencies. Directories are bind-mounted, - while other inputs are hard-linked (since only directories - can be bind-mounted). !!! As an extra security - precaution, make the fake Nix store only writable by the - build user. */ - Path chrootStoreDir = chrootRootDir + worker.store.storeDir; - createDirs(chrootStoreDir); - chmod_(chrootStoreDir, 01775); - - if (buildUser && chown(chrootStoreDir.c_str(), 0, buildUser->getGID()) == -1) - throw SysError("cannot change ownership of '%1%'", chrootStoreDir); - - for (auto & i : inputPaths) { - auto p = worker.store.printStorePath(i); - Path r = worker.store.toRealPath(p); - pathsInChroot.insert_or_assign(p, r); - } - - /* If we're repairing, checking or rebuilding part of a - multiple-outputs derivation, it's possible that we're - rebuilding a path that is in settings.sandbox-paths - (typically the dependencies of /bin/sh). Throw them - out. */ - for (auto & i : drv->outputsAndOptPaths(worker.store)) { - /* If the name isn't known a priori (i.e. floating - content-addressed derivation), the temporary location we use - should be fresh. Freshness means it is impossible that the path - is already in the sandbox, so we don't need to worry about - removing it. */ - if (i.second.second) - pathsInChroot.erase(worker.store.printStorePath(*i.second.second)); - } - - if (cgroup) { - if (mkdir(cgroup->c_str(), 0755) != 0) - throw SysError("creating cgroup '%s'", *cgroup); - chownToBuilder(*cgroup); - chownToBuilder(*cgroup + "/cgroup.procs"); - chownToBuilder(*cgroup + "/cgroup.threads"); - //chownToBuilder(*cgroup + "/cgroup.subtree_control"); - } - -#else - if (parsedDrv->useUidRange()) - throw Error("feature 'uid-range' is not supported on this platform"); - #if __APPLE__ - /* We don't really have any parent prep work to do (yet?) - All work happens in the child, instead. */ - #else - throw Error("sandboxing builds is not supported on this platform"); - #endif -#endif - } else { - if (parsedDrv->useUidRange()) - throw Error("feature 'uid-range' is only supported in sandboxed builds"); - } - - if (needsHashRewrite() && pathExists(homeDir)) - throw Error("home directory '%1%' exists; please remove it to assure purity of builds without sandboxing", homeDir); - - if (useChroot && settings.preBuildHook != "" && dynamic_cast(drv.get())) { - printMsg(lvlChatty, "executing pre-build hook '%1%'", settings.preBuildHook); - auto args = useChroot ? Strings({worker.store.printStorePath(drvPath), chrootRootDir}) : - Strings({ worker.store.printStorePath(drvPath) }); - enum BuildHookState { - stBegin, - stExtraChrootDirs - }; - auto state = stBegin; - auto lines = runProgram(settings.preBuildHook, false, args); - auto lastPos = std::string::size_type{0}; - for (auto nlPos = lines.find('\n'); nlPos != std::string::npos; - nlPos = lines.find('\n', lastPos)) - { - auto line = lines.substr(lastPos, nlPos - lastPos); - lastPos = nlPos + 1; - if (state == stBegin) { - if (line == "extra-sandbox-paths" || line == "extra-chroot-dirs") { - state = stExtraChrootDirs; - } else { - throw Error("unknown pre-build hook command '%1%'", line); - } - } else if (state == stExtraChrootDirs) { - if (line == "") { - state = stBegin; - } else { - auto p = line.find('='); - if (p == std::string::npos) - pathsInChroot[line] = line; - else - pathsInChroot[line.substr(0, p)] = line.substr(p + 1); - } - } - } - } - - /* Fire up a Nix daemon to process recursive Nix calls from the - builder. */ - if (parsedDrv->getRequiredSystemFeatures().count("recursive-nix")) - startDaemon(); - - /* Run the builder. */ - printMsg(lvlChatty, "executing builder '%1%'", drv->builder); - printMsg(lvlChatty, "using builder args '%1%'", concatStringsSep(" ", drv->args)); - for (auto & i : drv->env) - printMsg(lvlVomit, "setting builder env variable '%1%'='%2%'", i.first, i.second); - - /* Create the log file. */ - [[maybe_unused]] Path logFile = openLogFile(); - - /* Create a pseudoterminal to get the output of the builder. */ - builderOut = posix_openpt(O_RDWR | O_NOCTTY); - if (!builderOut) - throw SysError("opening pseudoterminal master"); - - // FIXME: not thread-safe, use ptsname_r - std::string slaveName = ptsname(builderOut.get()); - - if (buildUser) { - if (chmod(slaveName.c_str(), 0600)) - throw SysError("changing mode of pseudoterminal slave"); - - if (chown(slaveName.c_str(), buildUser->getUID(), 0)) - throw SysError("changing owner of pseudoterminal slave"); - } -#if __APPLE__ - else { - if (grantpt(builderOut.get())) - throw SysError("granting access to pseudoterminal slave"); - } -#endif - - if (unlockpt(builderOut.get())) - throw SysError("unlocking pseudoterminal"); - - /* Open the slave side of the pseudoterminal and use it as stderr. */ - auto openSlave = [&]() - { - AutoCloseFD builderOut = open(slaveName.c_str(), O_RDWR | O_NOCTTY); - if (!builderOut) - throw SysError("opening pseudoterminal slave"); - - // Put the pt into raw mode to prevent \n -> \r\n translation. - struct termios term; - if (tcgetattr(builderOut.get(), &term)) - throw SysError("getting pseudoterminal attributes"); - - cfmakeraw(&term); - - if (tcsetattr(builderOut.get(), TCSANOW, &term)) - throw SysError("putting pseudoterminal into raw mode"); - - if (dup2(builderOut.get(), STDERR_FILENO) == -1) - throw SysError("cannot pipe standard error into log file"); - }; - - buildResult.startTime = time(0); - - /* Fork a child to build the package. */ - -#if __linux__ - if (useChroot) { - /* Set up private namespaces for the build: - - - The PID namespace causes the build to start as PID 1. - Processes outside of the chroot are not visible to those - on the inside, but processes inside the chroot are - visible from the outside (though with different PIDs). - - - The private mount namespace ensures that all the bind - mounts we do will only show up in this process and its - children, and will disappear automatically when we're - done. - - - The private network namespace ensures that the builder - cannot talk to the outside world (or vice versa). It - only has a private loopback interface. (Fixed-output - derivations are not run in a private network namespace - to allow functions like fetchurl to work.) - - - The IPC namespace prevents the builder from communicating - with outside processes using SysV IPC mechanisms (shared - memory, message queues, semaphores). It also ensures - that all IPC objects are destroyed when the builder - exits. - - - The UTS namespace ensures that builders see a hostname of - localhost rather than the actual hostname. - - We use a helper process to do the clone() to work around - clone() being broken in multi-threaded programs due to - at-fork handlers not being run. Note that we use - CLONE_PARENT to ensure that the real builder is parented to - us. - */ - - if (derivationType->isSandboxed()) - privateNetwork = true; - - userNamespaceSync.create(); - - usingUserNamespace = userNamespacesSupported(); - - Pipe sendPid; - sendPid.create(); - - Pid helper = startProcess([&]() { - sendPid.readSide.close(); - - /* We need to open the slave early, before - CLONE_NEWUSER. Otherwise we get EPERM when running as - root. */ - openSlave(); - - try { - /* Drop additional groups here because we can't do it - after we've created the new user namespace. */ - if (setgroups(0, 0) == -1) { - if (errno != EPERM) - throw SysError("setgroups failed"); - if (settings.requireDropSupplementaryGroups) - throw Error("setgroups failed. Set the require-drop-supplementary-groups option to false to skip this step."); - } - - ProcessOptions options; - options.cloneFlags = CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWUTS | CLONE_PARENT | SIGCHLD; - if (privateNetwork) - options.cloneFlags |= CLONE_NEWNET; - if (usingUserNamespace) - options.cloneFlags |= CLONE_NEWUSER; - - pid_t child = startProcess([&]() { runChild(); }, options); - - writeFull(sendPid.writeSide.get(), fmt("%d\n", child)); - _exit(0); - } catch (...) { - handleChildException(true); - _exit(1); - } - }); - - sendPid.writeSide.close(); - - if (helper.wait() != 0) { - processSandboxSetupMessages(); - // Only reached if the child process didn't send an exception. - throw Error("unable to start build process"); - } - - userNamespaceSync.readSide = -1; - - /* Close the write side to prevent runChild() from hanging - reading from this. */ - Finally cleanup([&]() { - userNamespaceSync.writeSide = -1; - }); - - auto ss = tokenizeString>(readLine(sendPid.readSide.get())); - assert(ss.size() == 1); - pid = string2Int(ss[0]).value(); - - if (usingUserNamespace) { - /* Set the UID/GID mapping of the builder's user namespace - such that the sandbox user maps to the build user, or to - the calling user (if build users are disabled). */ - uid_t hostUid = buildUser ? buildUser->getUID() : getuid(); - uid_t hostGid = buildUser ? buildUser->getGID() : getgid(); - uid_t nrIds = buildUser ? buildUser->getUIDCount() : 1; - - writeFile("/proc/" + std::to_string(pid) + "/uid_map", - fmt("%d %d %d", sandboxUid(), hostUid, nrIds)); - - if (!buildUser || buildUser->getUIDCount() == 1) - writeFile("/proc/" + std::to_string(pid) + "/setgroups", "deny"); - - writeFile("/proc/" + std::to_string(pid) + "/gid_map", - fmt("%d %d %d", sandboxGid(), hostGid, nrIds)); - } else { - debug("note: not using a user namespace"); - if (!buildUser) - throw Error("cannot perform a sandboxed build because user namespaces are not enabled; check /proc/sys/user/max_user_namespaces"); - } - - /* Now that we now the sandbox uid, we can write - /etc/passwd. */ - writeFile(chrootRootDir + "/etc/passwd", fmt( - "root:x:0:0:Nix build user:%3%:/noshell\n" - "nixbld:x:%1%:%2%:Nix build user:%3%:/noshell\n" - "nobody:x:65534:65534:Nobody:/:/noshell\n", - sandboxUid(), sandboxGid(), settings.sandboxBuildDir)); - - /* Save the mount- and user namespace of the child. We have to do this - *before* the child does a chroot. */ - sandboxMountNamespace = open(fmt("/proc/%d/ns/mnt", (pid_t) pid).c_str(), O_RDONLY); - if (sandboxMountNamespace.get() == -1) - throw SysError("getting sandbox mount namespace"); - - if (usingUserNamespace) { - sandboxUserNamespace = open(fmt("/proc/%d/ns/user", (pid_t) pid).c_str(), O_RDONLY); - if (sandboxUserNamespace.get() == -1) - throw SysError("getting sandbox user namespace"); - } - - /* Move the child into its own cgroup. */ - if (cgroup) - writeFile(*cgroup + "/cgroup.procs", fmt("%d", (pid_t) pid)); - - /* Signal the builder that we've updated its user namespace. */ - writeFull(userNamespaceSync.writeSide.get(), "1"); - - } else -#endif - { - pid = startProcess([&]() { - openSlave(); - runChild(); - }); - } - - /* parent */ - pid.setSeparatePG(true); - worker.childStarted(shared_from_this(), {builderOut.get()}, true, true); - - processSandboxSetupMessages(); -} - - -void LocalDerivationGoal::processSandboxSetupMessages() -{ - std::vector msgs; - while (true) { - std::string msg = [&]() { - try { - return readLine(builderOut.get()); - } catch (Error & e) { - auto status = pid.wait(); - e.addTrace({}, "while waiting for the build environment for '%s' to initialize (%s, previous messages: %s)", - worker.store.printStorePath(drvPath), - statusToString(status), - concatStringsSep("|", msgs)); - throw; - } - }(); - if (msg.substr(0, 1) == "\2") break; - if (msg.substr(0, 1) == "\1") { - FdSource source(builderOut.get()); - auto ex = readError(source); - ex.addTrace({}, "while setting up the build environment"); - throw ex; - } - debug("sandbox setup: " + msg); - msgs.push_back(std::move(msg)); - } -} - - -void LocalDerivationGoal::initTmpDir() -{ - /* In a sandbox, for determinism, always use the same temporary - directory. */ -#if __linux__ - tmpDirInSandbox = useChroot ? settings.sandboxBuildDir : tmpDir; -#else - tmpDirInSandbox = tmpDir; -#endif - - /* In non-structured mode, add all bindings specified in the - derivation via the environment, except those listed in the - passAsFile attribute. Those are passed as file names pointing - to temporary files containing the contents. Note that - passAsFile is ignored in structure mode because it's not - needed (attributes are not passed through the environment, so - there is no size constraint). */ - if (!parsedDrv->getStructuredAttrs()) { - - StringSet passAsFile = tokenizeString(getOr(drv->env, "passAsFile", "")); - for (auto & i : drv->env) { - if (passAsFile.find(i.first) == passAsFile.end()) { - env[i.first] = i.second; - } else { - auto hash = hashString(HashAlgorithm::SHA256, i.first); - std::string fn = ".attr-" + hash.to_string(HashFormat::Nix32, false); - Path p = tmpDir + "/" + fn; - writeFile(p, rewriteStrings(i.second, inputRewrites)); - chownToBuilder(p); - env[i.first + "Path"] = tmpDirInSandbox + "/" + fn; - } - } - - } - - /* For convenience, set an environment pointing to the top build - directory. */ - env["NIX_BUILD_TOP"] = tmpDirInSandbox; - - /* Also set TMPDIR and variants to point to this directory. */ - env["TMPDIR"] = env["TEMPDIR"] = env["TMP"] = env["TEMP"] = tmpDirInSandbox; - - /* Explicitly set PWD to prevent problems with chroot builds. In - particular, dietlibc cannot figure out the cwd because the - inode of the current directory doesn't appear in .. (because - getdents returns the inode of the mount point). */ - env["PWD"] = tmpDirInSandbox; -} - - -void LocalDerivationGoal::initEnv() -{ - env.clear(); - - /* Most shells initialise PATH to some default (/bin:/usr/bin:...) when - PATH is not set. We don't want this, so we fill it in with some dummy - value. */ - env["PATH"] = "/path-not-set"; - - /* Set HOME to a non-existing path to prevent certain programs from using - /etc/passwd (or NIS, or whatever) to locate the home directory (for - example, wget looks for ~/.wgetrc). I.e., these tools use /etc/passwd - if HOME is not set, but they will just assume that the settings file - they are looking for does not exist if HOME is set but points to some - non-existing path. */ - env["HOME"] = homeDir; - - /* Tell the builder where the Nix store is. Usually they - shouldn't care, but this is useful for purity checking (e.g., - the compiler or linker might only want to accept paths to files - in the store or in the build directory). */ - env["NIX_STORE"] = worker.store.storeDir; - - /* The maximum number of cores to utilize for parallel building. */ - env["NIX_BUILD_CORES"] = fmt("%d", settings.buildCores); - - initTmpDir(); - - /* Compatibility hack with Nix <= 0.7: if this is a fixed-output - derivation, tell the builder, so that for instance `fetchurl' - can skip checking the output. On older Nixes, this environment - variable won't be set, so `fetchurl' will do the check. */ - if (derivationType->isFixed()) env["NIX_OUTPUT_CHECKED"] = "1"; - - /* *Only* if this is a fixed-output derivation, propagate the - values of the environment variables specified in the - `impureEnvVars' attribute to the builder. This allows for - instance environment variables for proxy configuration such as - `http_proxy' to be easily passed to downloaders like - `fetchurl'. Passing such environment variables from the caller - to the builder is generally impure, but the output of - fixed-output derivations is by definition pure (since we - already know the cryptographic hash of the output). */ - if (!derivationType->isSandboxed()) { - auto & impureEnv = settings.impureEnv.get(); - if (!impureEnv.empty()) - experimentalFeatureSettings.require(Xp::ConfigurableImpureEnv); - - for (auto & i : parsedDrv->getStringsAttr("impureEnvVars").value_or(Strings())) { - auto envVar = impureEnv.find(i); - if (envVar != impureEnv.end()) { - env[i] = envVar->second; - } else { - env[i] = getEnv(i).value_or(""); - } - } - } - - /* Currently structured log messages piggyback on stderr, but we - may change that in the future. So tell the builder which file - descriptor to use for that. */ - env["NIX_LOG_FD"] = "2"; - - /* Trigger colored output in various tools. */ - env["TERM"] = "xterm-256color"; -} - - -void LocalDerivationGoal::writeStructuredAttrs() -{ - if (auto structAttrsJson = parsedDrv->prepareStructuredAttrs(worker.store, inputPaths)) { - auto json = structAttrsJson.value(); - nlohmann::json rewritten; - for (auto & [i, v] : json["outputs"].get()) { - /* The placeholder must have a rewrite, so we use it to cover both the - cases where we know or don't know the output path ahead of time. */ - rewritten[i] = rewriteStrings((std::string) v, inputRewrites); - } - - json["outputs"] = rewritten; - - auto jsonSh = writeStructuredAttrsShell(json); - - writeFile(tmpDir + "/.attrs.sh", rewriteStrings(jsonSh, inputRewrites)); - chownToBuilder(tmpDir + "/.attrs.sh"); - env["NIX_ATTRS_SH_FILE"] = tmpDirInSandbox + "/.attrs.sh"; - writeFile(tmpDir + "/.attrs.json", rewriteStrings(json.dump(), inputRewrites)); - chownToBuilder(tmpDir + "/.attrs.json"); - env["NIX_ATTRS_JSON_FILE"] = tmpDirInSandbox + "/.attrs.json"; - } -} - - -static StorePath pathPartOfReq(const SingleDerivedPath & req) -{ - return std::visit(overloaded { - [&](const SingleDerivedPath::Opaque & bo) { - return bo.path; - }, - [&](const SingleDerivedPath::Built & bfd) { - return pathPartOfReq(*bfd.drvPath); - }, - }, req.raw()); -} - - -static StorePath pathPartOfReq(const DerivedPath & req) -{ - return std::visit(overloaded { - [&](const DerivedPath::Opaque & bo) { - return bo.path; - }, - [&](const DerivedPath::Built & bfd) { - return pathPartOfReq(*bfd.drvPath); - }, - }, req.raw()); -} - - -bool LocalDerivationGoal::isAllowed(const DerivedPath & req) -{ - return this->isAllowed(pathPartOfReq(req)); -} - - -struct RestrictedStoreConfig : virtual LocalFSStoreConfig -{ - using LocalFSStoreConfig::LocalFSStoreConfig; - const std::string name() override { return "Restricted Store"; } -}; - -/* A wrapper around LocalStore that only allows building/querying of - paths that are in the input closures of the build or were added via - recursive Nix calls. */ -struct RestrictedStore : public virtual RestrictedStoreConfig, public virtual IndirectRootStore, public virtual GcStore -{ - ref next; - - LocalDerivationGoal & goal; - - RestrictedStore(const Params & params, ref next, LocalDerivationGoal & goal) - : StoreConfig(params) - , LocalFSStoreConfig(params) - , RestrictedStoreConfig(params) - , Store(params) - , LocalFSStore(params) - , next(next), goal(goal) - { } - - Path getRealStoreDir() override - { return next->realStoreDir; } - - std::string getUri() override - { return next->getUri(); } - - StorePathSet queryAllValidPaths() override - { - StorePathSet paths; - for (auto & p : goal.inputPaths) paths.insert(p); - for (auto & p : goal.addedPaths) paths.insert(p); - return paths; - } - - void queryPathInfoUncached(const StorePath & path, - Callback> callback) noexcept override - { - if (goal.isAllowed(path)) { - try { - /* Censor impure information. */ - auto info = std::make_shared(*next->queryPathInfo(path)); - info->deriver.reset(); - info->registrationTime = 0; - info->ultimate = false; - info->sigs.clear(); - callback(info); - } catch (InvalidPath &) { - callback(nullptr); - } - } else - callback(nullptr); - }; - - void queryReferrers(const StorePath & path, StorePathSet & referrers) override - { } - - std::map> queryPartialDerivationOutputMap( - const StorePath & path, - Store * evalStore = nullptr) override - { - if (!goal.isAllowed(path)) - throw InvalidPath("cannot query output map for unknown path '%s' in recursive Nix", printStorePath(path)); - return next->queryPartialDerivationOutputMap(path, evalStore); - } - - std::optional queryPathFromHashPart(const std::string & hashPart) override - { throw Error("queryPathFromHashPart"); } - - StorePath addToStore( - std::string_view name, - const SourcePath & srcPath, - ContentAddressMethod method, - HashAlgorithm hashAlgo, - const StorePathSet & references, - PathFilter & filter, - RepairFlag repair) override - { throw Error("addToStore"); } - - void addToStore(const ValidPathInfo & info, Source & narSource, - RepairFlag repair = NoRepair, CheckSigsFlag checkSigs = CheckSigs) override - { - next->addToStore(info, narSource, repair, checkSigs); - goal.addDependency(info.path); - } - - StorePath addToStoreFromDump( - Source & dump, - std::string_view name, - FileSerialisationMethod dumpMethod, - ContentAddressMethod hashMethod, - HashAlgorithm hashAlgo, - const StorePathSet & references, - RepairFlag repair) override - { - auto path = next->addToStoreFromDump(dump, name, dumpMethod, hashMethod, hashAlgo, references, repair); - goal.addDependency(path); - return path; - } - - void narFromPath(const StorePath & path, Sink & sink) override - { - if (!goal.isAllowed(path)) - throw InvalidPath("cannot dump unknown path '%s' in recursive Nix", printStorePath(path)); - LocalFSStore::narFromPath(path, sink); - } - - void ensurePath(const StorePath & path) override - { - if (!goal.isAllowed(path)) - throw InvalidPath("cannot substitute unknown path '%s' in recursive Nix", printStorePath(path)); - /* Nothing to be done; 'path' must already be valid. */ - } - - void registerDrvOutput(const Realisation & info) override - // XXX: This should probably be allowed as a no-op if the realisation - // corresponds to an allowed derivation - { throw Error("registerDrvOutput"); } - - void queryRealisationUncached(const DrvOutput & id, - Callback> callback) noexcept override - // XXX: This should probably be allowed if the realisation corresponds to - // an allowed derivation - { - if (!goal.isAllowed(id)) - callback(nullptr); - next->queryRealisation(id, std::move(callback)); - } - - void buildPaths(const std::vector & paths, BuildMode buildMode, std::shared_ptr evalStore) override - { - for (auto & result : buildPathsWithResults(paths, buildMode, evalStore)) - if (!result.success()) - result.rethrow(); - } - - std::vector buildPathsWithResults( - const std::vector & paths, - BuildMode buildMode = bmNormal, - std::shared_ptr evalStore = nullptr) override - { - assert(!evalStore); - - if (buildMode != bmNormal) throw Error("unsupported build mode"); - - StorePathSet newPaths; - std::set newRealisations; - - for (auto & req : paths) { - if (!goal.isAllowed(req)) - throw InvalidPath("cannot build '%s' in recursive Nix because path is unknown", req.to_string(*next)); - } - - auto results = next->buildPathsWithResults(paths, buildMode); - - for (auto & result : results) { - for (auto & [outputName, output] : result.builtOutputs) { - newPaths.insert(output.outPath); - newRealisations.insert(output); - } - } - - StorePathSet closure; - next->computeFSClosure(newPaths, closure); - for (auto & path : closure) - goal.addDependency(path); - for (auto & real : Realisation::closure(*next, newRealisations)) - goal.addedDrvOutputs.insert(real.id); - - return results; - } - - BuildResult buildDerivation(const StorePath & drvPath, const BasicDerivation & drv, - BuildMode buildMode = bmNormal) override - { unsupported("buildDerivation"); } - - void addTempRoot(const StorePath & path) override - { } - - void addIndirectRoot(const Path & path) override - { } - - Roots findRoots(bool censor) override - { return Roots(); } - - void collectGarbage(const GCOptions & options, GCResults & results) override - { } - - void addSignatures(const StorePath & storePath, const StringSet & sigs) override - { unsupported("addSignatures"); } - - void queryMissing(const std::vector & targets, - StorePathSet & willBuild, StorePathSet & willSubstitute, StorePathSet & unknown, - uint64_t & downloadSize, uint64_t & narSize) override - { - /* This is slightly impure since it leaks information to the - client about what paths will be built/substituted or are - already present. Probably not a big deal. */ - - std::vector allowed; - for (auto & req : targets) { - if (goal.isAllowed(req)) - allowed.emplace_back(req); - else - unknown.insert(pathPartOfReq(req)); - } - - next->queryMissing(allowed, willBuild, willSubstitute, - unknown, downloadSize, narSize); - } - - virtual std::optional getBuildLogExact(const StorePath & path) override - { return std::nullopt; } - - virtual void addBuildLog(const StorePath & path, std::string_view log) override - { unsupported("addBuildLog"); } - - std::optional isTrustedClient() override - { return NotTrusted; } -}; - - -void LocalDerivationGoal::startDaemon() -{ - experimentalFeatureSettings.require(Xp::RecursiveNix); - - Store::Params params; - params["path-info-cache-size"] = "0"; - params["store"] = worker.store.storeDir; - if (auto & optRoot = getLocalStore().rootDir.get()) - params["root"] = *optRoot; - params["state"] = "/no-such-path"; - params["log"] = "/no-such-path"; - auto store = make_ref(params, - ref(std::dynamic_pointer_cast(worker.store.shared_from_this())), - *this); - - addedPaths.clear(); - - auto socketName = ".nix-socket"; - Path socketPath = tmpDir + "/" + socketName; - env["NIX_REMOTE"] = "unix://" + tmpDirInSandbox + "/" + socketName; - - daemonSocket = createUnixDomainSocket(socketPath, 0600); - - chownToBuilder(socketPath); - - daemonThread = std::thread([this, store]() { - - while (true) { - - /* Accept a connection. */ - struct sockaddr_un remoteAddr; - socklen_t remoteAddrLen = sizeof(remoteAddr); - - AutoCloseFD remote = accept(daemonSocket.get(), - (struct sockaddr *) &remoteAddr, &remoteAddrLen); - if (!remote) { - if (errno == EINTR || errno == EAGAIN) continue; - if (errno == EINVAL || errno == ECONNABORTED) break; - throw SysError("accepting connection"); - } - - unix::closeOnExec(remote.get()); - - debug("received daemon connection"); - - auto workerThread = std::thread([store, remote{std::move(remote)}]() { - try { - daemon::processConnection( - store, - FdSource(remote.get()), - FdSink(remote.get()), - NotTrusted, daemon::Recursive); - debug("terminated daemon connection"); - } catch (const Interrupted &) { - debug("interrupted daemon connection"); - } catch (SystemError &) { - ignoreExceptionExceptInterrupt(); - } - }); - - daemonWorkerThreads.push_back(std::move(workerThread)); - } - - debug("daemon shutting down"); - }); -} - - -void LocalDerivationGoal::stopDaemon() -{ - if (daemonSocket && shutdown(daemonSocket.get(), SHUT_RDWR) == -1) { - // According to the POSIX standard, the 'shutdown' function should - // return an ENOTCONN error when attempting to shut down a socket that - // hasn't been connected yet. This situation occurs when the 'accept' - // function is called on a socket without any accepted connections, - // leaving the socket unconnected. While Linux doesn't seem to produce - // an error for sockets that have only been accepted, more - // POSIX-compliant operating systems like OpenBSD, macOS, and others do - // return the ENOTCONN error. Therefore, we handle this error here to - // avoid raising an exception for compliant behaviour. - if (errno == ENOTCONN) { - daemonSocket.close(); - } else { - throw SysError("shutting down daemon socket"); - } - } - - if (daemonThread.joinable()) - daemonThread.join(); - - // FIXME: should prune worker threads more quickly. - // FIXME: shutdown the client socket to speed up worker termination. - for (auto & thread : daemonWorkerThreads) - thread.join(); - daemonWorkerThreads.clear(); - - // release the socket. - daemonSocket.close(); -} - - -void LocalDerivationGoal::addDependency(const StorePath & path) -{ - if (isAllowed(path)) return; - - addedPaths.insert(path); - - /* If we're doing a sandbox build, then we have to make the path - appear in the sandbox. */ - if (useChroot) { - - debug("materialising '%s' in the sandbox", worker.store.printStorePath(path)); - - #if __linux__ - - Path source = worker.store.Store::toRealPath(path); - Path target = chrootRootDir + worker.store.printStorePath(path); - - if (pathExists(target)) { - // There is a similar debug message in doBind, so only run it in this block to not have double messages. - debug("bind-mounting %s -> %s", target, source); - throw Error("store path '%s' already exists in the sandbox", worker.store.printStorePath(path)); - } - - /* Bind-mount the path into the sandbox. This requires - entering its mount namespace, which is not possible - in multithreaded programs. So we do this in a - child process.*/ - Pid child(startProcess([&]() { - - if (usingUserNamespace && (setns(sandboxUserNamespace.get(), 0) == -1)) - throw SysError("entering sandbox user namespace"); - - if (setns(sandboxMountNamespace.get(), 0) == -1) - throw SysError("entering sandbox mount namespace"); - - doBind(source, target); - - _exit(0); - })); - - int status = child.wait(); - if (status != 0) - throw Error("could not add path '%s' to sandbox", worker.store.printStorePath(path)); - - #else - throw Error("don't know how to make path '%s' (produced by a recursive Nix call) appear in the sandbox", - worker.store.printStorePath(path)); - #endif - - } -} - -void LocalDerivationGoal::chownToBuilder(const Path & path) -{ - if (!buildUser) return; - if (chown(path.c_str(), buildUser->getUID(), buildUser->getGID()) == -1) - throw SysError("cannot change ownership of '%1%'", path); -} - - -void setupSeccomp() -{ -#if __linux__ - if (!settings.filterSyscalls) return; -#if HAVE_SECCOMP - scmp_filter_ctx ctx; - - if (!(ctx = seccomp_init(SCMP_ACT_ALLOW))) - throw SysError("unable to initialize seccomp mode 2"); - - Finally cleanup([&]() { - seccomp_release(ctx); - }); - - constexpr std::string_view nativeSystem = SYSTEM; - - if (nativeSystem == "x86_64-linux" && - seccomp_arch_add(ctx, SCMP_ARCH_X86) != 0) - throw SysError("unable to add 32-bit seccomp architecture"); - - if (nativeSystem == "x86_64-linux" && - seccomp_arch_add(ctx, SCMP_ARCH_X32) != 0) - throw SysError("unable to add X32 seccomp architecture"); - - if (nativeSystem == "aarch64-linux" && - seccomp_arch_add(ctx, SCMP_ARCH_ARM) != 0) - printError("unable to add ARM seccomp architecture; this may result in spurious build failures if running 32-bit ARM processes"); - - if (nativeSystem == "mips64-linux" && - seccomp_arch_add(ctx, SCMP_ARCH_MIPS) != 0) - printError("unable to add mips seccomp architecture"); - - if (nativeSystem == "mips64-linux" && - seccomp_arch_add(ctx, SCMP_ARCH_MIPS64N32) != 0) - printError("unable to add mips64-*abin32 seccomp architecture"); - - if (nativeSystem == "mips64el-linux" && - seccomp_arch_add(ctx, SCMP_ARCH_MIPSEL) != 0) - printError("unable to add mipsel seccomp architecture"); - - if (nativeSystem == "mips64el-linux" && - seccomp_arch_add(ctx, SCMP_ARCH_MIPSEL64N32) != 0) - printError("unable to add mips64el-*abin32 seccomp architecture"); - - /* Prevent builders from creating setuid/setgid binaries. */ - for (int perm : { S_ISUID, S_ISGID }) { - if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(chmod), 1, - SCMP_A1(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) - throw SysError("unable to add seccomp rule"); - - if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(fchmod), 1, - SCMP_A1(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) - throw SysError("unable to add seccomp rule"); - - if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(fchmodat), 1, - SCMP_A2(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) - throw SysError("unable to add seccomp rule"); - - if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), NIX_SYSCALL_FCHMODAT2, 1, - SCMP_A2(SCMP_CMP_MASKED_EQ, (scmp_datum_t) perm, (scmp_datum_t) perm)) != 0) - throw SysError("unable to add seccomp rule"); - } - - /* Prevent builders from using EAs or ACLs. Not all filesystems - support these, and they're not allowed in the Nix store because - they're not representable in the NAR serialisation. */ - if (seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(getxattr), 0) != 0 || - seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(lgetxattr), 0) != 0 || - seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(fgetxattr), 0) != 0 || - seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(setxattr), 0) != 0 || - seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(lsetxattr), 0) != 0 || - seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ENOTSUP), SCMP_SYS(fsetxattr), 0) != 0) - throw SysError("unable to add seccomp rule"); - - if (seccomp_attr_set(ctx, SCMP_FLTATR_CTL_NNP, settings.allowNewPrivileges ? 0 : 1) != 0) - throw SysError("unable to set 'no new privileges' seccomp attribute"); - - if (seccomp_load(ctx) != 0) - throw SysError("unable to load seccomp BPF program"); -#else - throw Error( - "seccomp is not supported on this platform; " - "you can bypass this error by setting the option 'filter-syscalls' to false, but note that untrusted builds can then create setuid binaries!"); -#endif -#endif -} - - -void LocalDerivationGoal::runChild() -{ - /* Warning: in the child we should absolutely not make any SQLite - calls! */ - - bool sendException = true; - - try { /* child */ - - commonChildInit(); - - try { - setupSeccomp(); - } catch (...) { - if (buildUser) throw; - } - - bool setUser = true; - - /* Make the contents of netrc and the CA certificate bundle - available to builtin:fetchurl (which may run under a - different uid and/or in a sandbox). */ - std::string netrcData; - std::string caFileData; - if (drv->isBuiltin() && drv->builder == "builtin:fetchurl") { - try { - netrcData = readFile(settings.netrcFile); - } catch (SystemError &) { } - - try { - caFileData = readFile(settings.caFile); - } catch (SystemError &) { } - } - -#if __linux__ - if (useChroot) { - - userNamespaceSync.writeSide = -1; - - if (drainFD(userNamespaceSync.readSide.get()) != "1") - throw Error("user namespace initialisation failed"); - - userNamespaceSync.readSide = -1; - - if (privateNetwork) { - - /* Initialise the loopback interface. */ - AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP)); - if (!fd) throw SysError("cannot open IP socket"); - - struct ifreq ifr; - strcpy(ifr.ifr_name, "lo"); - ifr.ifr_flags = IFF_UP | IFF_LOOPBACK | IFF_RUNNING; - if (ioctl(fd.get(), SIOCSIFFLAGS, &ifr) == -1) - throw SysError("cannot set loopback interface flags"); - } - - /* Set the hostname etc. to fixed values. */ - char hostname[] = "localhost"; - if (sethostname(hostname, sizeof(hostname)) == -1) - throw SysError("cannot set host name"); - char domainname[] = "(none)"; // kernel default - if (setdomainname(domainname, sizeof(domainname)) == -1) - throw SysError("cannot set domain name"); - - /* Make all filesystems private. This is necessary - because subtrees may have been mounted as "shared" - (MS_SHARED). (Systemd does this, for instance.) Even - though we have a private mount namespace, mounting - filesystems on top of a shared subtree still propagates - outside of the namespace. Making a subtree private is - local to the namespace, though, so setting MS_PRIVATE - does not affect the outside world. */ - if (mount(0, "/", 0, MS_PRIVATE | MS_REC, 0) == -1) - throw SysError("unable to make '/' private"); - - /* Bind-mount chroot directory to itself, to treat it as a - different filesystem from /, as needed for pivot_root. */ - if (mount(chrootRootDir.c_str(), chrootRootDir.c_str(), 0, MS_BIND, 0) == -1) - throw SysError("unable to bind mount '%1%'", chrootRootDir); - - /* Bind-mount the sandbox's Nix store onto itself so that - we can mark it as a "shared" subtree, allowing bind - mounts made in *this* mount namespace to be propagated - into the child namespace created by the - unshare(CLONE_NEWNS) call below. - - Marking chrootRootDir as MS_SHARED causes pivot_root() - to fail with EINVAL. Don't know why. */ - Path chrootStoreDir = chrootRootDir + worker.store.storeDir; - - if (mount(chrootStoreDir.c_str(), chrootStoreDir.c_str(), 0, MS_BIND, 0) == -1) - throw SysError("unable to bind mount the Nix store", chrootStoreDir); - - if (mount(0, chrootStoreDir.c_str(), 0, MS_SHARED, 0) == -1) - throw SysError("unable to make '%s' shared", chrootStoreDir); - - /* Set up a nearly empty /dev, unless the user asked to - bind-mount the host /dev. */ - Strings ss; - if (pathsInChroot.find("/dev") == pathsInChroot.end()) { - createDirs(chrootRootDir + "/dev/shm"); - createDirs(chrootRootDir + "/dev/pts"); - ss.push_back("/dev/full"); - if (worker.store.systemFeatures.get().count("kvm") && pathExists("/dev/kvm")) - ss.push_back("/dev/kvm"); - ss.push_back("/dev/null"); - ss.push_back("/dev/random"); - ss.push_back("/dev/tty"); - ss.push_back("/dev/urandom"); - ss.push_back("/dev/zero"); - createSymlink("/proc/self/fd", chrootRootDir + "/dev/fd"); - createSymlink("/proc/self/fd/0", chrootRootDir + "/dev/stdin"); - createSymlink("/proc/self/fd/1", chrootRootDir + "/dev/stdout"); - createSymlink("/proc/self/fd/2", chrootRootDir + "/dev/stderr"); - } - - /* Fixed-output derivations typically need to access the - network, so give them access to /etc/resolv.conf and so - on. */ - if (!derivationType->isSandboxed()) { - // Only use nss functions to resolve hosts and - // services. Don’t use it for anything else that may - // be configured for this system. This limits the - // potential impurities introduced in fixed-outputs. - writeFile(chrootRootDir + "/etc/nsswitch.conf", "hosts: files dns\nservices: files\n"); - - /* N.B. it is realistic that these paths might not exist. It - happens when testing Nix building fixed-output derivations - within a pure derivation. */ - for (auto & path : { "/etc/resolv.conf", "/etc/services", "/etc/hosts" }) - if (pathExists(path)) - ss.push_back(path); - - if (settings.caFile != "" && pathExists(settings.caFile)) { - Path caFile = settings.caFile; - pathsInChroot.try_emplace("/etc/ssl/certs/ca-certificates.crt", canonPath(caFile, true), true); - } - } - - for (auto & i : ss) { - // For backwards-compatibiliy, resolve all the symlinks in the - // chroot paths - auto canonicalPath = canonPath(i, true); - pathsInChroot.emplace(i, canonicalPath); - } - - /* Bind-mount all the directories from the "host" - filesystem that we want in the chroot - environment. */ - for (auto & i : pathsInChroot) { - if (i.second.source == "/proc") continue; // backwards compatibility - - #if HAVE_EMBEDDED_SANDBOX_SHELL - if (i.second.source == "__embedded_sandbox_shell__") { - static unsigned char sh[] = { - #include "embedded-sandbox-shell.gen.hh" - }; - auto dst = chrootRootDir + i.first; - createDirs(dirOf(dst)); - writeFile(dst, std::string_view((const char *) sh, sizeof(sh))); - chmod_(dst, 0555); - } else - #endif - doBind(i.second.source, chrootRootDir + i.first, i.second.optional); - } - - /* Bind a new instance of procfs on /proc. */ - createDirs(chrootRootDir + "/proc"); - if (mount("none", (chrootRootDir + "/proc").c_str(), "proc", 0, 0) == -1) - throw SysError("mounting /proc"); - - /* Mount sysfs on /sys. */ - if (buildUser && buildUser->getUIDCount() != 1) { - createDirs(chrootRootDir + "/sys"); - if (mount("none", (chrootRootDir + "/sys").c_str(), "sysfs", 0, 0) == -1) - throw SysError("mounting /sys"); - } - - /* Mount a new tmpfs on /dev/shm to ensure that whatever - the builder puts in /dev/shm is cleaned up automatically. */ - if (pathExists("/dev/shm") && mount("none", (chrootRootDir + "/dev/shm").c_str(), "tmpfs", 0, - fmt("size=%s", settings.sandboxShmSize).c_str()) == -1) - throw SysError("mounting /dev/shm"); - - /* Mount a new devpts on /dev/pts. Note that this - requires the kernel to be compiled with - CONFIG_DEVPTS_MULTIPLE_INSTANCES=y (which is the case - if /dev/ptx/ptmx exists). */ - if (pathExists("/dev/pts/ptmx") && - !pathExists(chrootRootDir + "/dev/ptmx") - && !pathsInChroot.count("/dev/pts")) - { - if (mount("none", (chrootRootDir + "/dev/pts").c_str(), "devpts", 0, "newinstance,mode=0620") == 0) - { - createSymlink("/dev/pts/ptmx", chrootRootDir + "/dev/ptmx"); - - /* Make sure /dev/pts/ptmx is world-writable. With some - Linux versions, it is created with permissions 0. */ - chmod_(chrootRootDir + "/dev/pts/ptmx", 0666); - } else { - if (errno != EINVAL) - throw SysError("mounting /dev/pts"); - doBind("/dev/pts", chrootRootDir + "/dev/pts"); - doBind("/dev/ptmx", chrootRootDir + "/dev/ptmx"); - } - } - - /* Make /etc unwritable */ - if (!parsedDrv->useUidRange()) - chmod_(chrootRootDir + "/etc", 0555); - - /* Unshare this mount namespace. This is necessary because - pivot_root() below changes the root of the mount - namespace. This means that the call to setns() in - addDependency() would hide the host's filesystem, - making it impossible to bind-mount paths from the host - Nix store into the sandbox. Therefore, we save the - pre-pivot_root namespace in - sandboxMountNamespace. Since we made /nix/store a - shared subtree above, this allows addDependency() to - make paths appear in the sandbox. */ - if (unshare(CLONE_NEWNS) == -1) - throw SysError("unsharing mount namespace"); - - /* Unshare the cgroup namespace. This means - /proc/self/cgroup will show the child's cgroup as '/' - rather than whatever it is in the parent. */ - if (cgroup && unshare(CLONE_NEWCGROUP) == -1) - throw SysError("unsharing cgroup namespace"); - - /* Do the chroot(). */ - if (chdir(chrootRootDir.c_str()) == -1) - throw SysError("cannot change directory to '%1%'", chrootRootDir); - - if (mkdir("real-root", 0500) == -1) - throw SysError("cannot create real-root directory"); - - if (pivot_root(".", "real-root") == -1) - throw SysError("cannot pivot old root directory onto '%1%'", (chrootRootDir + "/real-root")); - - if (chroot(".") == -1) - throw SysError("cannot change root directory to '%1%'", chrootRootDir); - - if (umount2("real-root", MNT_DETACH) == -1) - throw SysError("cannot unmount real root filesystem"); - - if (rmdir("real-root") == -1) - throw SysError("cannot remove real-root directory"); - - /* Switch to the sandbox uid/gid in the user namespace, - which corresponds to the build user or calling user in - the parent namespace. */ - if (setgid(sandboxGid()) == -1) - throw SysError("setgid failed"); - if (setuid(sandboxUid()) == -1) - throw SysError("setuid failed"); - - setUser = false; - } -#endif - - if (chdir(tmpDirInSandbox.c_str()) == -1) - throw SysError("changing into '%1%'", tmpDir); - - /* Close all other file descriptors. */ - unix::closeExtraFDs(); - -#if __linux__ - linux::setPersonality(drv->platform); -#endif - - /* Disable core dumps by default. */ - struct rlimit limit = { 0, RLIM_INFINITY }; - setrlimit(RLIMIT_CORE, &limit); - - // FIXME: set other limits to deterministic values? - - /* Fill in the environment. */ - Strings envStrs; - for (auto & i : env) - envStrs.push_back(rewriteStrings(i.first + "=" + i.second, inputRewrites)); - - /* If we are running in `build-users' mode, then switch to the - user we allocated above. Make sure that we drop all root - privileges. Note that above we have closed all file - descriptors except std*, so that's safe. Also note that - setuid() when run as root sets the real, effective and - saved UIDs. */ - if (setUser && buildUser) { - /* Preserve supplementary groups of the build user, to allow - admins to specify groups such as "kvm". */ - auto gids = buildUser->getSupplementaryGIDs(); - if (setgroups(gids.size(), gids.data()) == -1) - throw SysError("cannot set supplementary groups of build user"); - - if (setgid(buildUser->getGID()) == -1 || - getgid() != buildUser->getGID() || - getegid() != buildUser->getGID()) - throw SysError("setgid failed"); - - if (setuid(buildUser->getUID()) == -1 || - getuid() != buildUser->getUID() || - geteuid() != buildUser->getUID()) - throw SysError("setuid failed"); - } - -#if __APPLE__ - /* This has to appear before import statements. */ - std::string sandboxProfile = "(version 1)\n"; - - if (useChroot) { - - /* Lots and lots and lots of file functions freak out if they can't stat their full ancestry */ - PathSet ancestry; - - /* We build the ancestry before adding all inputPaths to the store because we know they'll - all have the same parents (the store), and there might be lots of inputs. This isn't - particularly efficient... I doubt it'll be a bottleneck in practice */ - for (auto & i : pathsInChroot) { - Path cur = i.first; - while (cur.compare("/") != 0) { - cur = dirOf(cur); - ancestry.insert(cur); - } - } - - /* And we want the store in there regardless of how empty pathsInChroot. We include the innermost - path component this time, since it's typically /nix/store and we care about that. */ - Path cur = worker.store.storeDir; - while (cur.compare("/") != 0) { - ancestry.insert(cur); - cur = dirOf(cur); - } - - /* Add all our input paths to the chroot */ - for (auto & i : inputPaths) { - auto p = worker.store.printStorePath(i); - pathsInChroot[p] = p; - } - - /* Violations will go to the syslog if you set this. Unfortunately the destination does not appear to be configurable */ - if (settings.darwinLogSandboxViolations) { - sandboxProfile += "(deny default)\n"; - } else { - sandboxProfile += "(deny default (with no-log))\n"; - } - - sandboxProfile += - #include "sandbox-defaults.sb" - ; - - if (!derivationType->isSandboxed()) - sandboxProfile += - #include "sandbox-network.sb" - ; - - /* Add the output paths we'll use at build-time to the chroot */ - sandboxProfile += "(allow file-read* file-write* process-exec\n"; - for (auto & [_, path] : scratchOutputs) - sandboxProfile += fmt("\t(subpath \"%s\")\n", worker.store.printStorePath(path)); - - sandboxProfile += ")\n"; - - /* Our inputs (transitive dependencies and any impurities computed above) - - without file-write* allowed, access() incorrectly returns EPERM - */ - sandboxProfile += "(allow file-read* file-write* process-exec\n"; - for (auto & i : pathsInChroot) { - if (i.first != i.second.source) - throw Error( - "can't map '%1%' to '%2%': mismatched impure paths not supported on Darwin", - i.first, i.second.source); - - std::string path = i.first; - auto optSt = maybeLstat(path.c_str()); - if (!optSt) { - if (i.second.optional) - continue; - throw SysError("getting attributes of required path '%s", path); - } - if (S_ISDIR(optSt->st_mode)) - sandboxProfile += fmt("\t(subpath \"%s\")\n", path); - else - sandboxProfile += fmt("\t(literal \"%s\")\n", path); - } - sandboxProfile += ")\n"; - - /* Allow file-read* on full directory hierarchy to self. Allows realpath() */ - sandboxProfile += "(allow file-read*\n"; - for (auto & i : ancestry) { - sandboxProfile += fmt("\t(literal \"%s\")\n", i); - } - sandboxProfile += ")\n"; - - sandboxProfile += additionalSandboxProfile; - } else - sandboxProfile += - #include "sandbox-minimal.sb" - ; - - debug("Generated sandbox profile:"); - debug(sandboxProfile); - - bool allowLocalNetworking = parsedDrv->getBoolAttr("__darwinAllowLocalNetworking"); - - /* The tmpDir in scope points at the temporary build directory for our derivation. Some packages try different mechanisms - to find temporary directories, so we want to open up a broader place for them to put their files, if needed. */ - Path globalTmpDir = canonPath(defaultTempDir(), true); - - /* They don't like trailing slashes on subpath directives */ - while (!globalTmpDir.empty() && globalTmpDir.back() == '/') - globalTmpDir.pop_back(); - - if (getEnv("_NIX_TEST_NO_SANDBOX") != "1") { - Strings sandboxArgs; - sandboxArgs.push_back("_GLOBAL_TMP_DIR"); - sandboxArgs.push_back(globalTmpDir); - if (allowLocalNetworking) { - sandboxArgs.push_back("_ALLOW_LOCAL_NETWORKING"); - sandboxArgs.push_back("1"); - } - char * sandbox_errbuf = nullptr; - if (sandbox_init_with_parameters(sandboxProfile.c_str(), 0, stringsToCharPtrs(sandboxArgs).data(), &sandbox_errbuf)) { - writeFull(STDERR_FILENO, fmt("failed to configure sandbox: %s\n", sandbox_errbuf ? sandbox_errbuf : "(null)")); - _exit(1); - } - } -#endif - - /* Indicate that we managed to set up the build environment. */ - writeFull(STDERR_FILENO, std::string("\2\n")); - - sendException = false; - - /* Execute the program. This should not return. */ - if (drv->isBuiltin()) { - try { - logger = makeJSONLogger(*logger); - - std::map outputs; - for (auto & e : drv->outputs) - outputs.insert_or_assign(e.first, - worker.store.printStorePath(scratchOutputs.at(e.first))); - - if (drv->builder == "builtin:fetchurl") - builtinFetchurl(*drv, outputs, netrcData, caFileData); - else if (drv->builder == "builtin:buildenv") - builtinBuildenv(*drv, outputs); - else if (drv->builder == "builtin:unpack-channel") - builtinUnpackChannel(*drv, outputs); - else - throw Error("unsupported builtin builder '%1%'", drv->builder.substr(8)); - _exit(0); - } catch (std::exception & e) { - writeFull(STDERR_FILENO, e.what() + std::string("\n")); - _exit(1); - } - } - - // Now builder is not builtin - - Strings args; - args.push_back(std::string(baseNameOf(drv->builder))); - - for (auto & i : drv->args) - args.push_back(rewriteStrings(i, inputRewrites)); - -#if __APPLE__ - posix_spawnattr_t attrp; - - if (posix_spawnattr_init(&attrp)) - throw SysError("failed to initialize builder"); - - if (posix_spawnattr_setflags(&attrp, POSIX_SPAWN_SETEXEC)) - throw SysError("failed to initialize builder"); - - if (drv->platform == "aarch64-darwin") { - // Unset kern.curproc_arch_affinity so we can escape Rosetta - int affinity = 0; - sysctlbyname("kern.curproc_arch_affinity", NULL, NULL, &affinity, sizeof(affinity)); - - cpu_type_t cpu = CPU_TYPE_ARM64; - posix_spawnattr_setbinpref_np(&attrp, 1, &cpu, NULL); - } else if (drv->platform == "x86_64-darwin") { - cpu_type_t cpu = CPU_TYPE_X86_64; - posix_spawnattr_setbinpref_np(&attrp, 1, &cpu, NULL); - } - - posix_spawn(NULL, drv->builder.c_str(), NULL, &attrp, stringsToCharPtrs(args).data(), stringsToCharPtrs(envStrs).data()); -#else - execve(drv->builder.c_str(), stringsToCharPtrs(args).data(), stringsToCharPtrs(envStrs).data()); -#endif - - throw SysError("executing '%1%'", drv->builder); - - } catch (...) { - handleChildException(sendException); - _exit(1); - } -} - - -SingleDrvOutputs LocalDerivationGoal::registerOutputs() -{ - /* When using a build hook, the build hook can register the output - as valid (by doing `nix-store --import'). If so we don't have - to do anything here. - - We can only early return when the outputs are known a priori. For - floating content-addressed derivations this isn't the case. - */ - if (hook) - return DerivationGoal::registerOutputs(); - - std::map infos; - - /* Set of inodes seen during calls to canonicalisePathMetaData() - for this build's outputs. This needs to be shared between - outputs to allow hard links between outputs. */ - InodesSeen inodesSeen; - - Path checkSuffix = ".check"; - - std::exception_ptr delayedException; - - /* The paths that can be referenced are the input closures, the - output paths, and any paths that have been built via recursive - Nix calls. */ - StorePathSet referenceablePaths; - for (auto & p : inputPaths) referenceablePaths.insert(p); - for (auto & i : scratchOutputs) referenceablePaths.insert(i.second); - for (auto & p : addedPaths) referenceablePaths.insert(p); - - /* FIXME `needsHashRewrite` should probably be removed and we get to the - real reason why we aren't using the chroot dir */ - auto toRealPathChroot = [&](const Path & p) -> Path { - return useChroot && !needsHashRewrite() - ? chrootRootDir + p - : worker.store.toRealPath(p); - }; - - /* Check whether the output paths were created, and make all - output paths read-only. Then get the references of each output (that we - might need to register), so we can topologically sort them. For the ones - that are most definitely already installed, we just store their final - name so we can also use it in rewrites. */ - StringSet outputsToSort; - struct AlreadyRegistered { StorePath path; }; - struct PerhapsNeedToRegister { StorePathSet refs; }; - std::map> outputReferencesIfUnregistered; - std::map outputStats; - for (auto & [outputName, _] : drv->outputs) { - auto scratchOutput = get(scratchOutputs, outputName); - if (!scratchOutput) - throw BuildError( - "builder for '%s' has no scratch output for '%s'", - worker.store.printStorePath(drvPath), outputName); - auto actualPath = toRealPathChroot(worker.store.printStorePath(*scratchOutput)); - - outputsToSort.insert(outputName); - - /* Updated wanted info to remove the outputs we definitely don't need to register */ - auto initialOutput = get(initialOutputs, outputName); - if (!initialOutput) - throw BuildError( - "builder for '%s' has no initial output for '%s'", - worker.store.printStorePath(drvPath), outputName); - auto & initialInfo = *initialOutput; - - /* Don't register if already valid, and not checking */ - initialInfo.wanted = buildMode == bmCheck - || !(initialInfo.known && initialInfo.known->isValid()); - if (!initialInfo.wanted) { - outputReferencesIfUnregistered.insert_or_assign( - outputName, - AlreadyRegistered { .path = initialInfo.known->path }); - continue; - } - - auto optSt = maybeLstat(actualPath.c_str()); - if (!optSt) - throw BuildError( - "builder for '%s' failed to produce output path for output '%s' at '%s'", - worker.store.printStorePath(drvPath), outputName, actualPath); - struct stat & st = *optSt; - -#ifndef __CYGWIN__ - /* Check that the output is not group or world writable, as - that means that someone else can have interfered with the - build. Also, the output should be owned by the build - user. */ - if ((!S_ISLNK(st.st_mode) && (st.st_mode & (S_IWGRP | S_IWOTH))) || - (buildUser && st.st_uid != buildUser->getUID())) - throw BuildError( - "suspicious ownership or permission on '%s' for output '%s'; rejecting this build output", - actualPath, outputName); -#endif - - /* Canonicalise first. This ensures that the path we're - rewriting doesn't contain a hard link to /etc/shadow or - something like that. */ - canonicalisePathMetaData( - actualPath, - buildUser ? std::optional(buildUser->getUIDRange()) : std::nullopt, - inodesSeen); - - bool discardReferences = false; - if (auto structuredAttrs = parsedDrv->getStructuredAttrs()) { - if (auto udr = get(*structuredAttrs, "unsafeDiscardReferences")) { - if (auto output = get(*udr, outputName)) { - if (!output->is_boolean()) - throw Error("attribute 'unsafeDiscardReferences.\"%s\"' of derivation '%s' must be a Boolean", outputName, drvPath.to_string()); - discardReferences = output->get(); - } - } - } - - StorePathSet references; - if (discardReferences) - debug("discarding references of output '%s'", outputName); - else { - debug("scanning for references for output '%s' in temp location '%s'", outputName, actualPath); - - /* Pass blank Sink as we are not ready to hash data at this stage. */ - NullSink blank; - references = scanForReferences(blank, actualPath, referenceablePaths); - } - - outputReferencesIfUnregistered.insert_or_assign( - outputName, - PerhapsNeedToRegister { .refs = references }); - outputStats.insert_or_assign(outputName, std::move(st)); - } - - auto sortedOutputNames = topoSort(outputsToSort, - {[&](const std::string & name) { - auto orifu = get(outputReferencesIfUnregistered, name); - if (!orifu) - throw BuildError( - "no output reference for '%s' in build of '%s'", - name, worker.store.printStorePath(drvPath)); - return std::visit(overloaded { - /* Since we'll use the already installed versions of these, we - can treat them as leaves and ignore any references they - have. */ - [&](const AlreadyRegistered &) { return StringSet {}; }, - [&](const PerhapsNeedToRegister & refs) { - StringSet referencedOutputs; - /* FIXME build inverted map up front so no quadratic waste here */ - for (auto & r : refs.refs) - for (auto & [o, p] : scratchOutputs) - if (r == p) - referencedOutputs.insert(o); - return referencedOutputs; - }, - }, *orifu); - }}, - {[&](const std::string & path, const std::string & parent) { - // TODO with more -vvvv also show the temporary paths for manual inspection. - return BuildError( - "cycle detected in build of '%s' in the references of output '%s' from output '%s'", - worker.store.printStorePath(drvPath), path, parent); - }}); - - std::reverse(sortedOutputNames.begin(), sortedOutputNames.end()); - - OutputPathMap finalOutputs; - - for (auto & outputName : sortedOutputNames) { - auto output = get(drv->outputs, outputName); - auto scratchPath = get(scratchOutputs, outputName); - assert(output && scratchPath); - auto actualPath = toRealPathChroot(worker.store.printStorePath(*scratchPath)); - - auto finish = [&](StorePath finalStorePath) { - /* Store the final path */ - finalOutputs.insert_or_assign(outputName, finalStorePath); - /* The rewrite rule will be used in downstream outputs that refer to - use. This is why the topological sort is essential to do first - before this for loop. */ - if (*scratchPath != finalStorePath) - outputRewrites[std::string { scratchPath->hashPart() }] = std::string { finalStorePath.hashPart() }; - }; - - auto orifu = get(outputReferencesIfUnregistered, outputName); - assert(orifu); - - std::optional referencesOpt = std::visit(overloaded { - [&](const AlreadyRegistered & skippedFinalPath) -> std::optional { - finish(skippedFinalPath.path); - return std::nullopt; - }, - [&](const PerhapsNeedToRegister & r) -> std::optional { - return r.refs; - }, - }, *orifu); - - if (!referencesOpt) - continue; - auto references = *referencesOpt; - - auto rewriteOutput = [&](const StringMap & rewrites) { - /* Apply hash rewriting if necessary. */ - if (!rewrites.empty()) { - debug("rewriting hashes in '%1%'; cross fingers", actualPath); - - /* FIXME: Is this actually streaming? */ - auto source = sinkToSource([&](Sink & nextSink) { - RewritingSink rsink(rewrites, nextSink); - dumpPath(actualPath, rsink); - rsink.flush(); - }); - Path tmpPath = actualPath + ".tmp"; - restorePath(tmpPath, *source); - deletePath(actualPath); - movePath(tmpPath, actualPath); - - /* FIXME: set proper permissions in restorePath() so - we don't have to do another traversal. */ - canonicalisePathMetaData(actualPath, {}, inodesSeen); - } - }; - - auto rewriteRefs = [&]() -> StoreReferences { - /* In the CA case, we need the rewritten refs to calculate the - final path, therefore we look for a *non-rewritten - self-reference, and use a bool rather try to solve the - computationally intractable fixed point. */ - StoreReferences res { - .self = false, - }; - for (auto & r : references) { - auto name = r.name(); - auto origHash = std::string { r.hashPart() }; - if (r == *scratchPath) { - res.self = true; - } else if (auto outputRewrite = get(outputRewrites, origHash)) { - std::string newRef = *outputRewrite; - newRef += '-'; - newRef += name; - res.others.insert(StorePath { newRef }); - } else { - res.others.insert(r); - } - } - return res; - }; - - auto newInfoFromCA = [&](const DerivationOutput::CAFloating outputHash) -> ValidPathInfo { - auto st = get(outputStats, outputName); - if (!st) - throw BuildError( - "output path %1% without valid stats info", - actualPath); - if (outputHash.method.getFileIngestionMethod() == FileIngestionMethod::Flat) - { - /* The output path should be a regular file without execute permission. */ - if (!S_ISREG(st->st_mode) || (st->st_mode & S_IXUSR) != 0) - throw BuildError( - "output path '%1%' should be a non-executable regular file " - "since recursive hashing is not enabled (one of outputHashMode={flat,text} is true)", - actualPath); - } - rewriteOutput(outputRewrites); - /* FIXME optimize and deduplicate with addToStore */ - std::string oldHashPart { scratchPath->hashPart() }; - auto got = [&]{ - auto fim = outputHash.method.getFileIngestionMethod(); - switch (fim) { - case FileIngestionMethod::Flat: - case FileIngestionMethod::NixArchive: - { - HashModuloSink caSink { outputHash.hashAlgo, oldHashPart }; - auto fim = outputHash.method.getFileIngestionMethod(); - dumpPath( - {getFSSourceAccessor(), CanonPath(actualPath)}, - caSink, - (FileSerialisationMethod) fim); - return caSink.finish().first; - } - case FileIngestionMethod::Git: { - return git::dumpHash( - outputHash.hashAlgo, - {getFSSourceAccessor(), CanonPath(tmpDir + "/tmp")}).hash; - } - } - assert(false); - }(); - - ValidPathInfo newInfo0 { - worker.store, - outputPathName(drv->name, outputName), - ContentAddressWithReferences::fromParts( - outputHash.method, - std::move(got), - rewriteRefs()), - Hash::dummy, - }; - if (*scratchPath != newInfo0.path) { - // If the path has some self-references, we need to rewrite - // them. - // (note that this doesn't invalidate the ca hash we calculated - // above because it's computed *modulo the self-references*, so - // it already takes this rewrite into account). - rewriteOutput( - StringMap{{oldHashPart, - std::string(newInfo0.path.hashPart())}}); - } - - { - HashResult narHashAndSize = hashPath( - {getFSSourceAccessor(), CanonPath(actualPath)}, - FileSerialisationMethod::NixArchive, HashAlgorithm::SHA256); - newInfo0.narHash = narHashAndSize.first; - newInfo0.narSize = narHashAndSize.second; - } - - assert(newInfo0.ca); - return newInfo0; - }; - - ValidPathInfo newInfo = std::visit(overloaded { - - [&](const DerivationOutput::InputAddressed & output) { - /* input-addressed case */ - auto requiredFinalPath = output.path; - /* Preemptively add rewrite rule for final hash, as that is - what the NAR hash will use rather than normalized-self references */ - if (*scratchPath != requiredFinalPath) - outputRewrites.insert_or_assign( - std::string { scratchPath->hashPart() }, - std::string { requiredFinalPath.hashPart() }); - rewriteOutput(outputRewrites); - HashResult narHashAndSize = hashPath( - {getFSSourceAccessor(), CanonPath(actualPath)}, - FileSerialisationMethod::NixArchive, HashAlgorithm::SHA256); - ValidPathInfo newInfo0 { requiredFinalPath, narHashAndSize.first }; - newInfo0.narSize = narHashAndSize.second; - auto refs = rewriteRefs(); - newInfo0.references = std::move(refs.others); - if (refs.self) - newInfo0.references.insert(newInfo0.path); - return newInfo0; - }, - - [&](const DerivationOutput::CAFixed & dof) { - auto & wanted = dof.ca.hash; - - // Replace the output by a fresh copy of itself to make sure - // that there's no stale file descriptor pointing to it - Path tmpOutput = actualPath + ".tmp"; - copyFile( - std::filesystem::path(actualPath), - std::filesystem::path(tmpOutput), true); - - std::filesystem::rename(tmpOutput, actualPath); - - auto newInfo0 = newInfoFromCA(DerivationOutput::CAFloating { - .method = dof.ca.method, - .hashAlgo = wanted.algo, - }); - - /* Check wanted hash */ - assert(newInfo0.ca); - auto & got = newInfo0.ca->hash; - if (wanted != got) { - /* Throw an error after registering the path as - valid. */ - worker.hashMismatch = true; - delayedException = std::make_exception_ptr( - BuildError("hash mismatch in fixed-output derivation '%s':\n specified: %s\n got: %s", - worker.store.printStorePath(drvPath), - wanted.to_string(HashFormat::SRI, true), - got.to_string(HashFormat::SRI, true))); - } - if (!newInfo0.references.empty()) - delayedException = std::make_exception_ptr( - BuildError("illegal path references in fixed-output derivation '%s'", - worker.store.printStorePath(drvPath))); - - return newInfo0; - }, - - [&](const DerivationOutput::CAFloating & dof) { - return newInfoFromCA(dof); - }, - - [&](const DerivationOutput::Deferred &) -> ValidPathInfo { - // No derivation should reach that point without having been - // rewritten first - assert(false); - }, - - [&](const DerivationOutput::Impure & doi) { - return newInfoFromCA(DerivationOutput::CAFloating { - .method = doi.method, - .hashAlgo = doi.hashAlgo, - }); - }, - - }, output->raw); - - /* FIXME: set proper permissions in restorePath() so - we don't have to do another traversal. */ - canonicalisePathMetaData(actualPath, {}, inodesSeen); - - /* Calculate where we'll move the output files. In the checking case we - will leave leave them where they are, for now, rather than move to - their usual "final destination" */ - auto finalDestPath = worker.store.printStorePath(newInfo.path); - - /* Lock final output path, if not already locked. This happens with - floating CA derivations and hash-mismatching fixed-output - derivations. */ - PathLocks dynamicOutputLock; - dynamicOutputLock.setDeletion(true); - auto optFixedPath = output->path(worker.store, drv->name, outputName); - if (!optFixedPath || - worker.store.printStorePath(*optFixedPath) != finalDestPath) - { - assert(newInfo.ca); - dynamicOutputLock.lockPaths({worker.store.toRealPath(finalDestPath)}); - } - - /* Move files, if needed */ - if (worker.store.toRealPath(finalDestPath) != actualPath) { - if (buildMode == bmRepair) { - /* Path already exists, need to replace it */ - replaceValidPath(worker.store.toRealPath(finalDestPath), actualPath); - actualPath = worker.store.toRealPath(finalDestPath); - } else if (buildMode == bmCheck) { - /* Path already exists, and we want to compare, so we leave out - new path in place. */ - } else if (worker.store.isValidPath(newInfo.path)) { - /* Path already exists because CA path produced by something - else. No moving needed. */ - assert(newInfo.ca); - } else { - auto destPath = worker.store.toRealPath(finalDestPath); - deletePath(destPath); - movePath(actualPath, destPath); - actualPath = destPath; - } - } - - auto & localStore = getLocalStore(); - - if (buildMode == bmCheck) { - - if (!worker.store.isValidPath(newInfo.path)) continue; - ValidPathInfo oldInfo(*worker.store.queryPathInfo(newInfo.path)); - if (newInfo.narHash != oldInfo.narHash) { - worker.checkMismatch = true; - if (settings.runDiffHook || settings.keepFailed) { - auto dst = worker.store.toRealPath(finalDestPath + checkSuffix); - deletePath(dst); - movePath(actualPath, dst); - - handleDiffHook( - buildUser ? buildUser->getUID() : getuid(), - buildUser ? buildUser->getGID() : getgid(), - finalDestPath, dst, worker.store.printStorePath(drvPath), tmpDir); - - throw NotDeterministic("derivation '%s' may not be deterministic: output '%s' differs from '%s'", - worker.store.printStorePath(drvPath), worker.store.toRealPath(finalDestPath), dst); - } else - throw NotDeterministic("derivation '%s' may not be deterministic: output '%s' differs", - worker.store.printStorePath(drvPath), worker.store.toRealPath(finalDestPath)); - } - - /* Since we verified the build, it's now ultimately trusted. */ - if (!oldInfo.ultimate) { - oldInfo.ultimate = true; - localStore.signPathInfo(oldInfo); - localStore.registerValidPaths({{oldInfo.path, oldInfo}}); - } - - continue; - } - - /* For debugging, print out the referenced and unreferenced paths. */ - for (auto & i : inputPaths) { - if (references.count(i)) - debug("referenced input: '%1%'", worker.store.printStorePath(i)); - else - debug("unreferenced input: '%1%'", worker.store.printStorePath(i)); - } - - localStore.optimisePath(actualPath, NoRepair); // FIXME: combine with scanForReferences() - worker.markContentsGood(newInfo.path); - - newInfo.deriver = drvPath; - newInfo.ultimate = true; - localStore.signPathInfo(newInfo); - - finish(newInfo.path); - - /* If it's a CA path, register it right away. This is necessary if it - isn't statically known so that we can safely unlock the path before - the next iteration */ - if (newInfo.ca) - localStore.registerValidPaths({{newInfo.path, newInfo}}); - - infos.emplace(outputName, std::move(newInfo)); - } - - if (buildMode == bmCheck) { - /* In case of fixed-output derivations, if there are - mismatches on `--check` an error must be thrown as this is - also a source for non-determinism. */ - if (delayedException) - std::rethrow_exception(delayedException); - return assertPathValidity(); - } - - /* Apply output checks. */ - checkOutputs(infos); - - /* Register each output path as valid, and register the sets of - paths referenced by each of them. If there are cycles in the - outputs, this will fail. */ - { - auto & localStore = getLocalStore(); - - ValidPathInfos infos2; - for (auto & [outputName, newInfo] : infos) { - infos2.insert_or_assign(newInfo.path, newInfo); - } - localStore.registerValidPaths(infos2); - } - - /* In case of a fixed-output derivation hash mismatch, throw an - exception now that we have registered the output as valid. */ - if (delayedException) - std::rethrow_exception(delayedException); - - /* If we made it this far, we are sure the output matches the derivation - (since the delayedException would be a fixed output CA mismatch). That - means it's safe to link the derivation to the output hash. We must do - that for floating CA derivations, which otherwise couldn't be cached, - but it's fine to do in all cases. */ - SingleDrvOutputs builtOutputs; - - for (auto & [outputName, newInfo] : infos) { - auto oldinfo = get(initialOutputs, outputName); - assert(oldinfo); - auto thisRealisation = Realisation { - .id = DrvOutput { - oldinfo->outputHash, - outputName - }, - .outPath = newInfo.path - }; - if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations) - && !drv->type().isImpure()) - { - signRealisation(thisRealisation); - worker.store.registerDrvOutput(thisRealisation); - } - builtOutputs.emplace(outputName, thisRealisation); - } - - return builtOutputs; -} - -void LocalDerivationGoal::signRealisation(Realisation & realisation) -{ - getLocalStore().signRealisation(realisation); -} - - -void LocalDerivationGoal::checkOutputs(const std::map & outputs) -{ - std::map outputsByPath; - for (auto & output : outputs) - outputsByPath.emplace(worker.store.printStorePath(output.second.path), output.second); - - for (auto & output : outputs) { - auto & outputName = output.first; - auto & info = output.second; - - struct Checks - { - bool ignoreSelfRefs = false; - std::optional maxSize, maxClosureSize; - std::optional allowedReferences, allowedRequisites, disallowedReferences, disallowedRequisites; - }; - - /* Compute the closure and closure size of some output. This - is slightly tricky because some of its references (namely - other outputs) may not be valid yet. */ - auto getClosure = [&](const StorePath & path) - { - uint64_t closureSize = 0; - StorePathSet pathsDone; - std::queue pathsLeft; - pathsLeft.push(path); - - while (!pathsLeft.empty()) { - auto path = pathsLeft.front(); - pathsLeft.pop(); - if (!pathsDone.insert(path).second) continue; - - auto i = outputsByPath.find(worker.store.printStorePath(path)); - if (i != outputsByPath.end()) { - closureSize += i->second.narSize; - for (auto & ref : i->second.references) - pathsLeft.push(ref); - } else { - auto info = worker.store.queryPathInfo(path); - closureSize += info->narSize; - for (auto & ref : info->references) - pathsLeft.push(ref); - } - } - - return std::make_pair(std::move(pathsDone), closureSize); - }; - - auto applyChecks = [&](const Checks & checks) - { - if (checks.maxSize && info.narSize > *checks.maxSize) - throw BuildError("path '%s' is too large at %d bytes; limit is %d bytes", - worker.store.printStorePath(info.path), info.narSize, *checks.maxSize); - - if (checks.maxClosureSize) { - uint64_t closureSize = getClosure(info.path).second; - if (closureSize > *checks.maxClosureSize) - throw BuildError("closure of path '%s' is too large at %d bytes; limit is %d bytes", - worker.store.printStorePath(info.path), closureSize, *checks.maxClosureSize); - } - - auto checkRefs = [&](const std::optional & value, bool allowed, bool recursive) - { - if (!value) return; - - /* Parse a list of reference specifiers. Each element must - either be a store path, or the symbolic name of the output - of the derivation (such as `out'). */ - StorePathSet spec; - for (auto & i : *value) { - if (worker.store.isStorePath(i)) - spec.insert(worker.store.parseStorePath(i)); - else if (auto output = get(outputs, i)) - spec.insert(output->path); - else - throw BuildError("derivation contains an illegal reference specifier '%s'", i); - } - - auto used = recursive - ? getClosure(info.path).first - : info.references; - - if (recursive && checks.ignoreSelfRefs) - used.erase(info.path); - - StorePathSet badPaths; - - for (auto & i : used) - if (allowed) { - if (!spec.count(i)) - badPaths.insert(i); - } else { - if (spec.count(i)) - badPaths.insert(i); - } - - if (!badPaths.empty()) { - std::string badPathsStr; - for (auto & i : badPaths) { - badPathsStr += "\n "; - badPathsStr += worker.store.printStorePath(i); - } - throw BuildError("output '%s' is not allowed to refer to the following paths:%s", - worker.store.printStorePath(info.path), badPathsStr); - } - }; - - checkRefs(checks.allowedReferences, true, false); - checkRefs(checks.allowedRequisites, true, true); - checkRefs(checks.disallowedReferences, false, false); - checkRefs(checks.disallowedRequisites, false, true); - }; - - if (auto structuredAttrs = parsedDrv->getStructuredAttrs()) { - if (get(*structuredAttrs, "allowedReferences")){ - warn("'structuredAttrs' disables the effect of the top-level attribute 'allowedReferences'; use 'outputChecks' instead"); - } - if (get(*structuredAttrs, "allowedRequisites")){ - warn("'structuredAttrs' disables the effect of the top-level attribute 'allowedRequisites'; use 'outputChecks' instead"); - } - if (get(*structuredAttrs, "disallowedRequisites")){ - warn("'structuredAttrs' disables the effect of the top-level attribute 'disallowedRequisites'; use 'outputChecks' instead"); - } - if (get(*structuredAttrs, "disallowedReferences")){ - warn("'structuredAttrs' disables the effect of the top-level attribute 'disallowedReferences'; use 'outputChecks' instead"); - } - if (get(*structuredAttrs, "maxSize")){ - warn("'structuredAttrs' disables the effect of the top-level attribute 'maxSize'; use 'outputChecks' instead"); - } - if (get(*structuredAttrs, "maxClosureSize")){ - warn("'structuredAttrs' disables the effect of the top-level attribute 'maxClosureSize'; use 'outputChecks' instead"); - } - if (auto outputChecks = get(*structuredAttrs, "outputChecks")) { - if (auto output = get(*outputChecks, outputName)) { - Checks checks; - - if (auto maxSize = get(*output, "maxSize")) - checks.maxSize = maxSize->get(); - - if (auto maxClosureSize = get(*output, "maxClosureSize")) - checks.maxClosureSize = maxClosureSize->get(); - - auto get_ = [&](const std::string & name) -> std::optional { - if (auto i = get(*output, name)) { - Strings res; - for (auto j = i->begin(); j != i->end(); ++j) { - if (!j->is_string()) - throw Error("attribute '%s' of derivation '%s' must be a list of strings", name, worker.store.printStorePath(drvPath)); - res.push_back(j->get()); - } - checks.disallowedRequisites = res; - return res; - } - return {}; - }; - - checks.allowedReferences = get_("allowedReferences"); - checks.allowedRequisites = get_("allowedRequisites"); - checks.disallowedReferences = get_("disallowedReferences"); - checks.disallowedRequisites = get_("disallowedRequisites"); - - applyChecks(checks); - } - } - } else { - // legacy non-structured-attributes case - Checks checks; - checks.ignoreSelfRefs = true; - checks.allowedReferences = parsedDrv->getStringsAttr("allowedReferences"); - checks.allowedRequisites = parsedDrv->getStringsAttr("allowedRequisites"); - checks.disallowedReferences = parsedDrv->getStringsAttr("disallowedReferences"); - checks.disallowedRequisites = parsedDrv->getStringsAttr("disallowedRequisites"); - applyChecks(checks); - } - } -} - - -void LocalDerivationGoal::deleteTmpDir(bool force) -{ - if (topTmpDir != "") { - /* Don't keep temporary directories for builtins because they - might have privileged stuff (like a copy of netrc). */ - if (settings.keepFailed && !force && !drv->isBuiltin()) { - printError("note: keeping build directory '%s'", tmpDir); - chmod(topTmpDir.c_str(), 0755); - chmod(tmpDir.c_str(), 0755); - } - else - deletePath(topTmpDir); - topTmpDir = ""; - tmpDir = ""; - } -} - - -bool LocalDerivationGoal::isReadDesc(int fd) -{ - return (hook && DerivationGoal::isReadDesc(fd)) || - (!hook && fd == builderOut.get()); -} - - -StorePath LocalDerivationGoal::makeFallbackPath(OutputNameView outputName) -{ - // This is a bogus path type, constructed this way to ensure that it doesn't collide with any other store path - // See doc/manual/source/protocols/store-path.md for details - // TODO: We may want to separate the responsibilities of constructing the path fingerprint and of actually doing the hashing - auto pathType = "rewrite:" + std::string(drvPath.to_string()) + ":name:" + std::string(outputName); - return worker.store.makeStorePath( - pathType, - // pass an all-zeroes hash - Hash(HashAlgorithm::SHA256), outputPathName(drv->name, outputName)); -} - - -StorePath LocalDerivationGoal::makeFallbackPath(const StorePath & path) -{ - // This is a bogus path type, constructed this way to ensure that it doesn't collide with any other store path - // See doc/manual/source/protocols/store-path.md for details - auto pathType = "rewrite:" + std::string(drvPath.to_string()) + ":" + std::string(path.to_string()); - return worker.store.makeStorePath( - pathType, - // pass an all-zeroes hash - Hash(HashAlgorithm::SHA256), path.name()); -} - - -} diff --git a/src/libstore/unix/build/local-derivation-goal.hh b/src/libstore/unix/build/local-derivation-goal.hh deleted file mode 100644 index 1ea2476610a..00000000000 --- a/src/libstore/unix/build/local-derivation-goal.hh +++ /dev/null @@ -1,324 +0,0 @@ -#pragma once -///@file - -#include "derivation-goal.hh" -#include "local-store.hh" -#include "processes.hh" - -namespace nix { - -struct LocalDerivationGoal : public DerivationGoal -{ - LocalStore & getLocalStore(); - - /** - * User selected for running the builder. - */ - std::unique_ptr buildUser; - - /** - * The process ID of the builder. - */ - Pid pid; - - /** - * The cgroup of the builder, if any. - */ - std::optional cgroup; - - /** - * The temporary directory used for the build. - */ - Path tmpDir; - - /** - * The top-level temporary directory. `tmpDir` is either equal to - * or a child of this directory. - */ - Path topTmpDir; - - /** - * The path of the temporary directory in the sandbox. - */ - Path tmpDirInSandbox; - - /** - * Master side of the pseudoterminal used for the builder's - * standard output/error. - */ - AutoCloseFD builderOut; - - /** - * Pipe for synchronising updates to the builder namespaces. - */ - Pipe userNamespaceSync; - - /** - * The mount namespace and user namespace of the builder, used to add additional - * paths to the sandbox as a result of recursive Nix calls. - */ - AutoCloseFD sandboxMountNamespace; - AutoCloseFD sandboxUserNamespace; - - /** - * On Linux, whether we're doing the build in its own user - * namespace. - */ - bool usingUserNamespace = true; - - /** - * Whether we're currently doing a chroot build. - */ - bool useChroot = false; - - /** - * The parent directory of `chrootRootDir`. It has permission 700 - * and is owned by root to ensure other users cannot mess with - * `chrootRootDir`. - */ - Path chrootParentDir; - - /** - * The root of the chroot environment. - */ - Path chrootRootDir; - - /** - * RAII object to delete the chroot directory. - */ - std::shared_ptr autoDelChroot; - - /** - * Whether to run the build in a private network namespace. - */ - bool privateNetwork = false; - - /** - * Stuff we need to pass to initChild(). - */ - struct ChrootPath { - Path source; - bool optional; - ChrootPath(Path source = "", bool optional = false) - : source(source), optional(optional) - { } - }; - typedef map PathsInChroot; // maps target path to source path - PathsInChroot pathsInChroot; - - typedef map Environment; - Environment env; - -#if __APPLE__ - typedef std::string SandboxProfile; - SandboxProfile additionalSandboxProfile; -#endif - - /** - * Hash rewriting. - */ - StringMap inputRewrites, outputRewrites; - typedef map RedirectedOutputs; - RedirectedOutputs redirectedOutputs; - - /** - * The output paths used during the build. - * - * - Input-addressed derivations or fixed content-addressed outputs are - * sometimes built when some of their outputs already exist, and can not - * be hidden via sandboxing. We use temporary locations instead and - * rewrite after the build. Otherwise the regular predetermined paths are - * put here. - * - * - Floating content-addressed derivations do not know their final build - * output paths until the outputs are hashed, so random locations are - * used, and then renamed. The randomness helps guard against hidden - * self-references. - */ - OutputPathMap scratchOutputs; - - uid_t sandboxUid() { return usingUserNamespace ? (!buildUser || buildUser->getUIDCount() == 1 ? 1000 : 0) : buildUser->getUID(); } - gid_t sandboxGid() { return usingUserNamespace ? (!buildUser || buildUser->getUIDCount() == 1 ? 100 : 0) : buildUser->getGID(); } - - const static Path homeDir; - - /** - * The recursive Nix daemon socket. - */ - AutoCloseFD daemonSocket; - - /** - * The daemon main thread. - */ - std::thread daemonThread; - - /** - * The daemon worker threads. - */ - std::vector daemonWorkerThreads; - - /** - * Paths that were added via recursive Nix calls. - */ - StorePathSet addedPaths; - - /** - * Realisations that were added via recursive Nix calls. - */ - std::set addedDrvOutputs; - - /** - * Recursive Nix calls are only allowed to build or realize paths - * in the original input closure or added via a recursive Nix call - * (so e.g. you can't do 'nix-store -r /nix/store/' where - * /nix/store/ is some arbitrary path in a binary cache). - */ - bool isAllowed(const StorePath & path) - { - return inputPaths.count(path) || addedPaths.count(path); - } - bool isAllowed(const DrvOutput & id) - { - return addedDrvOutputs.count(id); - } - - bool isAllowed(const DerivedPath & req); - - friend struct RestrictedStore; - - using DerivationGoal::DerivationGoal; - - virtual ~LocalDerivationGoal() override; - - /** - * Whether we need to perform hash rewriting if there are valid output paths. - */ - bool needsHashRewrite(); - - /** - * The additional states. - */ - Goal::Co tryLocalBuild() override; - - /** - * Start building a derivation. - */ - void startBuilder(); - - /** - * Fill in the environment for the builder. - */ - void initEnv(); - - /** - * Process messages send by the sandbox initialization. - */ - void processSandboxSetupMessages(); - - /** - * Setup tmp dir location. - */ - void initTmpDir(); - - /** - * Write a JSON file containing the derivation attributes. - */ - void writeStructuredAttrs(); - - /** - * Start an in-process nix daemon thread for recursive-nix. - */ - void startDaemon(); - - /** - * Stop the in-process nix daemon thread. - * @see startDaemon - */ - void stopDaemon(); - - /** - * Add 'path' to the set of paths that may be referenced by the - * outputs, and make it appear in the sandbox. - */ - void addDependency(const StorePath & path); - - /** - * Make a file owned by the builder. - */ - void chownToBuilder(const Path & path); - - int getChildStatus() override; - - /** - * Run the builder's process. - */ - void runChild(); - - /** - * Check that the derivation outputs all exist and register them - * as valid. - */ - SingleDrvOutputs registerOutputs() override; - - void signRealisation(Realisation &) override; - - /** - * Check that an output meets the requirements specified by the - * 'outputChecks' attribute (or the legacy - * '{allowed,disallowed}{References,Requisites}' attributes). - */ - void checkOutputs(const std::map & outputs); - - /** - * Close the read side of the logger pipe. - */ - void closeReadPipes() override; - - /** - * Cleanup hooks for buildDone() - */ - void cleanupHookFinally() override; - void cleanupPreChildKill() override; - void cleanupPostChildKill() override; - bool cleanupDecideWhetherDiskFull() override; - void cleanupPostOutputsRegisteredModeCheck() override; - void cleanupPostOutputsRegisteredModeNonCheck() override; - - bool isReadDesc(int fd) override; - - /** - * Delete the temporary directory, if we have one. - */ - void deleteTmpDir(bool force); - - /** - * Forcibly kill the child process, if any. - * - * Called by destructor, can't be overridden - */ - void killChild() override final; - - /** - * Kill any processes running under the build user UID or in the - * cgroup of the build. - */ - void killSandbox(bool getStats); - - /** - * Create alternative path calculated from but distinct from the - * input, so we can avoid overwriting outputs (or other store paths) - * that already exist. - */ - StorePath makeFallbackPath(const StorePath & path); - - /** - * Make a path to another based on the output name along with the - * derivation hash. - * - * @todo Add option to randomize, so we can audit whether our - * rewrites caught everything - */ - StorePath makeFallbackPath(OutputNameView outputName); -}; - -} diff --git a/src/libstore/unix/build/sandbox-defaults.sb b/src/libstore/unix/build/sandbox-defaults.sb index 15cd6daf5e0..dd6a064c1bd 100644 --- a/src/libstore/unix/build/sandbox-defaults.sb +++ b/src/libstore/unix/build/sandbox-defaults.sb @@ -29,12 +29,14 @@ R""( ; Allow getpwuid. (allow mach-lookup (global-name "com.apple.system.opendirectoryd.libinfo")) -; Access to /tmp. +; Access to /tmp and the build directory. ; The network-outbound/network-inbound ones are for unix domain sockets, which ; we allow access to in TMPDIR (but if we allow them more broadly, you could in ; theory escape the sandbox) (allow file* process-exec network-outbound network-inbound - (literal "/tmp") (subpath TMPDIR)) + (literal "/tmp") + (subpath TMPDIR) + (subpath (param "_NIX_BUILD_TOP"))) ; Some packages like to read the system version. (allow file-read* diff --git a/src/libstore/unix/build/child.hh b/src/libstore/unix/include/nix/store/build/child.hh similarity index 100% rename from src/libstore/unix/build/child.hh rename to src/libstore/unix/include/nix/store/build/child.hh diff --git a/src/libstore/unix/include/nix/store/build/derivation-builder.hh b/src/libstore/unix/include/nix/store/build/derivation-builder.hh new file mode 100644 index 00000000000..5ce38e034eb --- /dev/null +++ b/src/libstore/unix/include/nix/store/build/derivation-builder.hh @@ -0,0 +1,196 @@ +#pragma once +///@file + +#include "nix/store/build-result.hh" +#include "nix/store/derivation-options.hh" +#include "nix/store/build/derivation-building-misc.hh" +#include "nix/store/derivations.hh" +#include "nix/store/parsed-derivations.hh" +#include "nix/util/processes.hh" +#include "nix/store/restricted-store.hh" +#include "nix/store/user-lock.hh" + +namespace nix { + +/** + * Parameters by (mostly) `const` reference for `DerivationBuilder`. + */ +struct DerivationBuilderParams +{ + /** The path of the derivation. */ + const StorePath & drvPath; + + BuildResult & buildResult; + + /** + * The derivation stored at drvPath. + */ + const Derivation & drv; + + /** + * The "structured attrs" of `drv`, if it has them. + * + * @todo this should be part of `Derivation`. + * + * @todo this should be renamed from `parsedDrv`. + */ + const StructuredAttrs * parsedDrv; + + /** + * The derivation options of `drv`. + * + * @todo this should be part of `Derivation`. + */ + const DerivationOptions & drvOptions; + + // The remainder is state held during the build. + + /** + * All input paths (that is, the union of FS closures of the + * immediate input paths). + */ + const StorePathSet & inputPaths; + + /** + * @note we do in fact mutate this + */ + std::map & initialOutputs; + + const BuildMode & buildMode; + + DerivationBuilderParams( + const StorePath & drvPath, + const BuildMode & buildMode, + BuildResult & buildResult, + const Derivation & drv, + const StructuredAttrs * parsedDrv, + const DerivationOptions & drvOptions, + const StorePathSet & inputPaths, + std::map & initialOutputs) + : drvPath{drvPath} + , buildResult{buildResult} + , drv{drv} + , parsedDrv{parsedDrv} + , drvOptions{drvOptions} + , inputPaths{inputPaths} + , initialOutputs{initialOutputs} + , buildMode{buildMode} + { } + + DerivationBuilderParams(DerivationBuilderParams &&) = default; +}; + +/** + * Callbacks that `DerivationBuilder` needs. + */ +struct DerivationBuilderCallbacks +{ + virtual ~DerivationBuilderCallbacks() = default; + + /** + * Open a log file and a pipe to it. + */ + virtual Path openLogFile() = 0; + + /** + * Close the log file. + */ + virtual void closeLogFile() = 0; + + virtual void appendLogTailErrorMsg(std::string & msg) = 0; + + /** + * Hook up `builderOut` to some mechanism to ingest the log + * + * @todo this should be reworked + */ + virtual void childStarted(Descriptor builderOut) = 0; + + /** + * @todo this should be reworked + */ + virtual void childTerminated() = 0; + + virtual void noteHashMismatch(void) = 0; + virtual void noteCheckMismatch(void) = 0; + + virtual void markContentsGood(const StorePath & path) = 0; +}; + +/** + * This class represents the state for building locally. + * + * @todo Ideally, it would not be a class, but a single function. + * However, besides the main entry point, there are a few more methods + * which are externally called, and need to be gotten rid of. There are + * also some virtual methods (either directly here or inherited from + * `DerivationBuilderCallbacks`, a stop-gap) that represent outgoing + * rather than incoming call edges that either should be removed, or + * become (higher order) function parameters. + */ +struct DerivationBuilder : RestrictionContext +{ + /** + * The process ID of the builder. + */ + Pid pid; + + DerivationBuilder() = default; + virtual ~DerivationBuilder() = default; + + /** + * Master side of the pseudoterminal used for the builder's + * standard output/error. + */ + AutoCloseFD builderOut; + + /** + * Set up build environment / sandbox, acquiring resources (e.g. + * locks as needed). After this is run, the builder should be + * started. + * + * @returns true if successful, false if we could not acquire a build + * user. In that case, the caller must wait and then try again. + */ + virtual bool prepareBuild() = 0; + + /** + * Start building a derivation. + */ + virtual void startBuilder() = 0; + + /** + * Tear down build environment after the builder exits (either on + * its own or if it is killed). + * + * @returns The first case indicates failure during output + * processing. A status code and exception are returned, providing + * more information. The second case indicates success, and + * realisations for each output of the derivation are returned. + */ + virtual std::variant, SingleDrvOutputs> unprepareBuild() = 0; + + /** + * Stop the in-process nix daemon thread. + * @see startDaemon + */ + virtual void stopDaemon() = 0; + + /** + * Delete the temporary directory, if we have one. + */ + virtual void deleteTmpDir(bool force) = 0; + + /** + * Kill any processes running under the build user UID or in the + * cgroup of the build. + */ + virtual void killSandbox(bool getStats) = 0; +}; + +std::unique_ptr makeDerivationBuilder( + Store & store, + std::unique_ptr miscMethods, + DerivationBuilderParams params); + +} diff --git a/src/libstore/unix/build/hook-instance.hh b/src/libstore/unix/include/nix/store/build/hook-instance.hh similarity index 83% rename from src/libstore/unix/build/hook-instance.hh rename to src/libstore/unix/include/nix/store/build/hook-instance.hh index 61cf534f4e9..ff205ff7698 100644 --- a/src/libstore/unix/build/hook-instance.hh +++ b/src/libstore/unix/include/nix/store/build/hook-instance.hh @@ -1,9 +1,9 @@ #pragma once ///@file -#include "logging.hh" -#include "serialise.hh" -#include "processes.hh" +#include "nix/util/logging.hh" +#include "nix/util/serialise.hh" +#include "nix/util/processes.hh" namespace nix { diff --git a/src/libstore/unix/include/nix/store/meson.build b/src/libstore/unix/include/nix/store/meson.build new file mode 100644 index 00000000000..7cf973223d0 --- /dev/null +++ b/src/libstore/unix/include/nix/store/meson.build @@ -0,0 +1,8 @@ +include_dirs += include_directories('../..') + +headers += files( + 'build/child.hh', + 'build/derivation-builder.hh', + 'build/hook-instance.hh', + 'user-lock.hh', +) diff --git a/src/libstore/unix/user-lock.hh b/src/libstore/unix/include/nix/store/user-lock.hh similarity index 100% rename from src/libstore/unix/user-lock.hh rename to src/libstore/unix/include/nix/store/user-lock.hh diff --git a/src/libstore/unix/meson.build b/src/libstore/unix/meson.build index d9d19013107..4b8a6b10518 100644 --- a/src/libstore/unix/meson.build +++ b/src/libstore/unix/meson.build @@ -1,19 +1,9 @@ sources += files( 'build/child.cc', + 'build/derivation-builder.cc', 'build/hook-instance.cc', - 'build/local-derivation-goal.cc', 'pathlocks.cc', 'user-lock.cc', ) -include_dirs += include_directories( - '.', - 'build', -) - -headers += files( - 'build/child.hh', - 'build/hook-instance.hh', - 'build/local-derivation-goal.hh', - 'user-lock.hh', -) +subdir('include/nix/store') diff --git a/src/libstore/unix/pathlocks.cc b/src/libstore/unix/pathlocks.cc index 1ec4579ec96..58d047f4e00 100644 --- a/src/libstore/unix/pathlocks.cc +++ b/src/libstore/unix/pathlocks.cc @@ -1,7 +1,7 @@ -#include "pathlocks.hh" -#include "util.hh" -#include "sync.hh" -#include "signals.hh" +#include "nix/store/pathlocks.hh" +#include "nix/util/util.hh" +#include "nix/util/sync.hh" +#include "nix/util/signals.hh" #include #include diff --git a/src/libstore/unix/user-lock.cc b/src/libstore/unix/user-lock.cc index 29f4b2cb31c..f5d164e5b18 100644 --- a/src/libstore/unix/user-lock.cc +++ b/src/libstore/unix/user-lock.cc @@ -2,15 +2,16 @@ #include #include -#include "user-lock.hh" -#include "file-system.hh" -#include "globals.hh" -#include "pathlocks.hh" -#include "users.hh" +#include "nix/store/user-lock.hh" +#include "nix/util/file-system.hh" +#include "nix/store/globals.hh" +#include "nix/store/pathlocks.hh" +#include "nix/util/users.hh" +#include "nix/util/logging.hh" namespace nix { -#if __linux__ +#ifdef __linux__ static std::vector get_group_list(const char *username, gid_t group_id) { @@ -94,7 +95,7 @@ struct SimpleUserLock : UserLock if (lock->uid == getuid() || lock->uid == geteuid()) throw Error("the Nix user should not be a member of '%s'", settings.buildUsersGroup); - #if __linux__ + #ifdef __linux__ /* Get the list of supplementary groups of this user. This is * usually either empty or contains a group such as "kvm". */ @@ -193,10 +194,10 @@ std::unique_ptr acquireUserLock(uid_t nrIds, bool useUserNamespace) bool useBuildUsers() { - #if __linux__ + #ifdef __linux__ static bool b = (settings.buildUsersGroup != "" || settings.autoAllocateUids) && isRootUser(); return b; - #elif __APPLE__ + #elif defined(__APPLE__) || defined(__FreeBSD__) static bool b = settings.buildUsersGroup != "" && isRootUser(); return b; #else diff --git a/src/libstore/windows/pathlocks.cc b/src/libstore/windows/pathlocks.cc index 00761a8c33c..92a7cbcf9fd 100644 --- a/src/libstore/windows/pathlocks.cc +++ b/src/libstore/windows/pathlocks.cc @@ -1,11 +1,13 @@ -#include "logging.hh" -#include "pathlocks.hh" -#include "signals.hh" -#include "util.hh" -#include -#include -#include -#include "windows-error.hh" +#include "nix/util/logging.hh" +#include "nix/store/pathlocks.hh" +#include "nix/util/signals.hh" +#include "nix/util/util.hh" + +#ifdef _WIN32 +# include +# include +# include +# include "nix/util/windows-error.hh" namespace nix { @@ -125,7 +127,7 @@ bool PathLocks::lockPaths(const PathSet & paths, const std::string & waitMsg, bo } } - debug("lock aquired on '%1%'", lockPath); + debug("lock acquired on '%1%'", lockPath); struct _stat st; if (_fstat(fromDescriptorReadOnly(fd.get()), &st) == -1) @@ -154,3 +156,4 @@ FdLock::FdLock(Descriptor desc, LockType lockType, bool wait, std::string_view w } } +#endif diff --git a/src/libstore/worker-protocol-connection.cc b/src/libstore/worker-protocol-connection.cc index 6585df4be62..d07dc816380 100644 --- a/src/libstore/worker-protocol-connection.cc +++ b/src/libstore/worker-protocol-connection.cc @@ -1,11 +1,11 @@ -#include "worker-protocol-connection.hh" -#include "worker-protocol-impl.hh" -#include "build-result.hh" -#include "derivations.hh" +#include "nix/store/worker-protocol-connection.hh" +#include "nix/store/worker-protocol-impl.hh" +#include "nix/store/build-result.hh" +#include "nix/store/derivations.hh" namespace nix { -const std::set WorkerProto::allFeatures{}; +const WorkerProto::FeatureSet WorkerProto::allFeatures{}; WorkerProto::BasicClientConnection::~BasicClientConnection() { @@ -146,21 +146,20 @@ void WorkerProto::BasicClientConnection::processStderr( } } -static std::set -intersectFeatures(const std::set & a, const std::set & b) +static WorkerProto::FeatureSet intersectFeatures(const WorkerProto::FeatureSet & a, const WorkerProto::FeatureSet & b) { - std::set res; + WorkerProto::FeatureSet res; for (auto & x : a) if (b.contains(x)) res.insert(x); return res; } -std::tuple> WorkerProto::BasicClientConnection::handshake( +std::tuple WorkerProto::BasicClientConnection::handshake( BufferedSink & to, Source & from, WorkerProto::Version localVersion, - const std::set & supportedFeatures) + const WorkerProto::FeatureSet & supportedFeatures) { to << WORKER_MAGIC_1 << localVersion; to.flush(); @@ -178,21 +177,21 @@ std::tuple> WorkerProto::Ba auto protoVersion = std::min(daemonVersion, localVersion); /* Exchange features. */ - std::set daemonFeatures; + WorkerProto::FeatureSet daemonFeatures; if (GET_PROTOCOL_MINOR(protoVersion) >= 38) { to << supportedFeatures; to.flush(); - daemonFeatures = readStrings>(from); + daemonFeatures = readStrings(from); } return {protoVersion, intersectFeatures(daemonFeatures, supportedFeatures)}; } -std::tuple> WorkerProto::BasicServerConnection::handshake( +std::tuple WorkerProto::BasicServerConnection::handshake( BufferedSink & to, Source & from, WorkerProto::Version localVersion, - const std::set & supportedFeatures) + const WorkerProto::FeatureSet & supportedFeatures) { unsigned int magic = readInt(from); if (magic != WORKER_MAGIC_1) @@ -204,9 +203,9 @@ std::tuple> WorkerProto::Ba auto protoVersion = std::min(clientVersion, localVersion); /* Exchange features. */ - std::set clientFeatures; + WorkerProto::FeatureSet clientFeatures; if (GET_PROTOCOL_MINOR(protoVersion) >= 38) { - clientFeatures = readStrings>(from); + clientFeatures = readStrings(from); to << supportedFeatures; to.flush(); } diff --git a/src/libstore/worker-protocol.cc b/src/libstore/worker-protocol.cc index f06fb2893c7..21b21a3478d 100644 --- a/src/libstore/worker-protocol.cc +++ b/src/libstore/worker-protocol.cc @@ -1,11 +1,11 @@ -#include "serialise.hh" -#include "path-with-outputs.hh" -#include "store-api.hh" -#include "build-result.hh" -#include "worker-protocol.hh" -#include "worker-protocol-impl.hh" -#include "archive.hh" -#include "path-info.hh" +#include "nix/util/serialise.hh" +#include "nix/store/path-with-outputs.hh" +#include "nix/store/store-api.hh" +#include "nix/store/build-result.hh" +#include "nix/store/worker-protocol.hh" +#include "nix/store/worker-protocol-impl.hh" +#include "nix/util/archive.hh" +#include "nix/store/path-info.hh" #include #include diff --git a/src/libutil-c/build-utils-meson b/src/libutil-c/build-utils-meson deleted file mode 120000 index 5fff21bab55..00000000000 --- a/src/libutil-c/build-utils-meson +++ /dev/null @@ -1 +0,0 @@ -../../build-utils-meson \ No newline at end of file diff --git a/src/libutil-c/meson.build b/src/libutil-c/meson.build index d44453676e9..3414a6d31c1 100644 --- a/src/libutil-c/meson.build +++ b/src/libutil-c/meson.build @@ -4,8 +4,6 @@ project('nix-util-c', 'cpp', 'cpp_std=c++2a', # TODO(Qyriad): increase the warning level 'warning_level=1', - 'debug=true', - 'optimization=2', 'errorlogs=true', # Please print logs for tests that fail ], meson_version : '>= 1.1', @@ -14,7 +12,7 @@ project('nix-util-c', 'cpp', cxx = meson.get_compiler('cpp') -subdir('build-utils-meson/deps-lists') +subdir('nix-meson-build-support/deps-lists') configdata = configuration_data() @@ -23,29 +21,16 @@ deps_private_maybe_subproject = [ ] deps_public_maybe_subproject = [ ] -subdir('build-utils-meson/subprojects') +subdir('nix-meson-build-support/subprojects') -# TODO rename, because it will conflict with downstream projects configdata.set_quoted('PACKAGE_VERSION', meson.project_version()) -config_h = configure_file( +config_priv_h = configure_file( configuration : configdata, - output : 'config-util.h', + output : 'nix_api_util_config.h', ) -add_project_arguments( - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. - - # From C++ libraries, only for internals - '-include', 'config-util.hh', - - # From C libraries, for our public, installed headers too - '-include', 'config-util.h', - language : 'cpp', -) - -subdir('build-utils-meson/common') +subdir('nix-meson-build-support/common') sources = files( 'nix_api_util.cc', @@ -53,19 +38,20 @@ sources = files( include_dirs = [include_directories('.')] -headers = [config_h] + files( +headers = files( 'nix_api_util.h', ) # TODO don't install this once tests don't use it. headers += files('nix_api_util_internal.h') -subdir('build-utils-meson/export-all-symbols') -subdir('build-utils-meson/windows-version') +subdir('nix-meson-build-support/export-all-symbols') +subdir('nix-meson-build-support/windows-version') this_library = library( 'nixutilc', sources, + config_priv_h, dependencies : deps_public + deps_private + deps_other, include_directories : include_dirs, link_args: linker_export_flags, @@ -73,8 +59,8 @@ this_library = library( install : true, ) -install_headers(headers, subdir : 'nix', preserve_path : true) +install_headers(headers, preserve_path : true) libraries_private = [] -subdir('build-utils-meson/export') +subdir('nix-meson-build-support/export') diff --git a/src/libutil-c/nix-meson-build-support b/src/libutil-c/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/libutil-c/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/libutil-c/nix_api_util.cc b/src/libutil-c/nix_api_util.cc index 992ea0a2ad0..2254f18fa97 100644 --- a/src/libutil-c/nix_api_util.cc +++ b/src/libutil-c/nix_api_util.cc @@ -1,12 +1,14 @@ #include "nix_api_util.h" -#include "config-global.hh" -#include "error.hh" +#include "nix/util/config-global.hh" +#include "nix/util/error.hh" #include "nix_api_util_internal.h" -#include "util.hh" +#include "nix/util/util.hh" #include #include +#include "nix_api_util_config.h" + nix_c_context * nix_c_context_create() { return new nix_c_context(); diff --git a/src/libutil-c/nix_api_util.h b/src/libutil-c/nix_api_util.h index 43f9fa9dc63..5f42641d426 100644 --- a/src/libutil-c/nix_api_util.h +++ b/src/libutil-c/nix_api_util.h @@ -47,7 +47,7 @@ extern "C" { */ // Error codes /** - * @brief Type for error codes in the NIX system + * @brief Type for error codes in the Nix system * * This type can have one of several predefined constants: * - NIX_OK: No error occurred (0) diff --git a/src/libutil-c/nix_api_util_internal.h b/src/libutil-c/nix_api_util_internal.h index 7fa4252acfd..8fbf3d91a06 100644 --- a/src/libutil-c/nix_api_util_internal.h +++ b/src/libutil-c/nix_api_util_internal.h @@ -4,7 +4,7 @@ #include #include -#include "error.hh" +#include "nix/util/error.hh" #include "nix_api_util.h" struct nix_c_context diff --git a/src/libutil-c/package.nix b/src/libutil-c/package.nix index 35533f98136..f26f57775d4 100644 --- a/src/libutil-c/package.nix +++ b/src/libutil-c/package.nix @@ -1,12 +1,12 @@ -{ lib -, stdenv -, mkMesonLibrary +{ + lib, + mkMesonLibrary, -, nix-util + nix-util, -# Configuration Options + # Configuration Options -, version + version, }: let @@ -19,8 +19,8 @@ mkMesonLibrary (finalAttrs: { workDir = ./.; fileset = fileset.unions [ - ../../build-utils-meson - ./build-utils-meson + ../../nix-meson-build-support + ./nix-meson-build-support ../../.version ./.version ./meson.build @@ -34,21 +34,9 @@ mkMesonLibrary (finalAttrs: { nix-util ]; - preConfigure = - # "Inline" .version so it's not a symlink, and includes the suffix. - # Do the meson utils, without modification. - '' - chmod u+w ./.version - echo ${version} > ../../.version - ''; - mesonFlags = [ ]; - env = lib.optionalAttrs (stdenv.isLinux && !(stdenv.hostPlatform.isStatic && stdenv.system == "aarch64-linux")) { - LDFLAGS = "-fuse-ld=gold"; - }; - meta = { platforms = lib.platforms.unix ++ lib.platforms.windows; }; diff --git a/src/libutil-test-support/build-utils-meson b/src/libutil-test-support/build-utils-meson deleted file mode 120000 index 5fff21bab55..00000000000 --- a/src/libutil-test-support/build-utils-meson +++ /dev/null @@ -1 +0,0 @@ -../../build-utils-meson \ No newline at end of file diff --git a/src/libutil-test-support/tests/hash.cc b/src/libutil-test-support/hash.cc similarity index 89% rename from src/libutil-test-support/tests/hash.cc rename to src/libutil-test-support/hash.cc index 51b9663b4c4..d047f4073df 100644 --- a/src/libutil-test-support/tests/hash.cc +++ b/src/libutil-test-support/hash.cc @@ -2,9 +2,9 @@ #include -#include "hash.hh" +#include "nix/util/hash.hh" -#include "tests/hash.hh" +#include "nix/util/tests/hash.hh" namespace rc { using namespace nix; diff --git a/src/libutil-test-support/tests/characterization.hh b/src/libutil-test-support/include/nix/util/tests/characterization.hh similarity index 95% rename from src/libutil-test-support/tests/characterization.hh rename to src/libutil-test-support/include/nix/util/tests/characterization.hh index 5e790e75ba6..3e8effe8b61 100644 --- a/src/libutil-test-support/tests/characterization.hh +++ b/src/libutil-test-support/include/nix/util/tests/characterization.hh @@ -3,9 +3,9 @@ #include -#include "types.hh" -#include "environment-variables.hh" -#include "file-system.hh" +#include "nix/util/types.hh" +#include "nix/util/environment-variables.hh" +#include "nix/util/file-system.hh" namespace nix { diff --git a/src/libutil-test-support/tests/gtest-with-params.hh b/src/libutil-test-support/include/nix/util/tests/gtest-with-params.hh similarity index 100% rename from src/libutil-test-support/tests/gtest-with-params.hh rename to src/libutil-test-support/include/nix/util/tests/gtest-with-params.hh diff --git a/src/libutil-test-support/tests/hash.hh b/src/libutil-test-support/include/nix/util/tests/hash.hh similarity index 86% rename from src/libutil-test-support/tests/hash.hh rename to src/libutil-test-support/include/nix/util/tests/hash.hh index 1f9fa59ae9b..de832c12f86 100644 --- a/src/libutil-test-support/tests/hash.hh +++ b/src/libutil-test-support/include/nix/util/tests/hash.hh @@ -3,7 +3,7 @@ #include -#include +#include "nix/util/hash.hh" namespace rc { using namespace nix; diff --git a/src/libutil-test-support/include/nix/util/tests/meson.build b/src/libutil-test-support/include/nix/util/tests/meson.build new file mode 100644 index 00000000000..f77dedff7e4 --- /dev/null +++ b/src/libutil-test-support/include/nix/util/tests/meson.build @@ -0,0 +1,11 @@ +# Public headers directory + +include_dirs = [include_directories('../../..')] + +headers = files( + 'characterization.hh', + 'gtest-with-params.hh', + 'hash.hh', + 'nix_api_util.hh', + 'string_callback.hh', +) diff --git a/src/libutil-test-support/tests/nix_api_util.hh b/src/libutil-test-support/include/nix/util/tests/nix_api_util.hh similarity index 54% rename from src/libutil-test-support/tests/nix_api_util.hh rename to src/libutil-test-support/include/nix/util/tests/nix_api_util.hh index 006dc497ccf..382c7b292fd 100644 --- a/src/libutil-test-support/tests/nix_api_util.hh +++ b/src/libutil-test-support/include/nix/util/tests/nix_api_util.hh @@ -3,6 +3,7 @@ #include "nix_api_util.h" #include +#include namespace nixC { @@ -24,7 +25,12 @@ protected: nix_c_context * ctx; - inline void assert_ctx_ok() + inline std::string loc(const char * file, int line) + { + return std::string(file) + ":" + std::to_string(line); + } + + inline void assert_ctx_ok(const char * file, int line) { if (nix_err_code(ctx) == NIX_OK) { return; @@ -32,16 +38,18 @@ protected: unsigned int n; const char * p = nix_err_msg(nullptr, ctx, &n); std::string msg(p, n); - throw std::runtime_error(std::string("nix_err_code(ctx) != NIX_OK, message: ") + msg); + throw std::runtime_error(loc(file, line) + ": nix_err_code(ctx) != NIX_OK, message: " + msg); } +#define assert_ctx_ok() assert_ctx_ok(__FILE__, __LINE__) - inline void assert_ctx_err() + inline void assert_ctx_err(const char * file, int line) { if (nix_err_code(ctx) != NIX_OK) { return; } - throw std::runtime_error("Got NIX_OK, but expected an error!"); + throw std::runtime_error(loc(file, line) + ": Got NIX_OK, but expected an error!"); } +#define assert_ctx_err() assert_ctx_err(__FILE__, __LINE__) }; } diff --git a/src/libutil-test-support/tests/string_callback.hh b/src/libutil-test-support/include/nix/util/tests/string_callback.hh similarity index 100% rename from src/libutil-test-support/tests/string_callback.hh rename to src/libutil-test-support/include/nix/util/tests/string_callback.hh diff --git a/src/libutil-test-support/meson.build b/src/libutil-test-support/meson.build index 4afed01ca0d..ec6bc15d9ac 100644 --- a/src/libutil-test-support/meson.build +++ b/src/libutil-test-support/meson.build @@ -4,8 +4,6 @@ project('nix-util-test-support', 'cpp', 'cpp_std=c++2a', # TODO(Qyriad): increase the warning level 'warning_level=1', - 'debug=true', - 'optimization=2', 'errorlogs=true', # Please print logs for tests that fail ], meson_version : '>= 1.1', @@ -14,7 +12,7 @@ project('nix-util-test-support', 'cpp', cxx = meson.get_compiler('cpp') -subdir('build-utils-meson/deps-lists') +subdir('nix-meson-build-support/deps-lists') deps_private_maybe_subproject = [ ] @@ -22,37 +20,22 @@ deps_public_maybe_subproject = [ dependency('nix-util'), dependency('nix-util-c'), ] -subdir('build-utils-meson/subprojects') +subdir('nix-meson-build-support/subprojects') rapidcheck = dependency('rapidcheck') deps_public += rapidcheck -add_project_arguments( - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. - '-include', 'config-util.hh', - language : 'cpp', -) - -subdir('build-utils-meson/common') +subdir('nix-meson-build-support/common') sources = files( - 'tests/hash.cc', - 'tests/string_callback.cc', + 'hash.cc', + 'string_callback.cc', ) -include_dirs = [include_directories('.')] - -headers = files( - 'tests/characterization.hh', - 'tests/gtest-with-params.hh', - 'tests/hash.hh', - 'tests/nix_api_util.hh', - 'tests/string_callback.hh', -) +subdir('include/nix/util/tests') -subdir('build-utils-meson/export-all-symbols') -subdir('build-utils-meson/windows-version') +subdir('nix-meson-build-support/export-all-symbols') +subdir('nix-meson-build-support/windows-version') this_library = library( 'nix-util-test-support', @@ -66,8 +49,8 @@ this_library = library( install : true, ) -install_headers(headers, subdir : 'nix', preserve_path : true) +install_headers(headers, subdir : 'nix/util/tests', preserve_path : true) libraries_private = [] -subdir('build-utils-meson/export') +subdir('nix-meson-build-support/export') diff --git a/src/libutil-test-support/nix-meson-build-support b/src/libutil-test-support/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/libutil-test-support/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/libutil-test-support/package.nix b/src/libutil-test-support/package.nix index c403e762c1a..f8e92c27113 100644 --- a/src/libutil-test-support/package.nix +++ b/src/libutil-test-support/package.nix @@ -1,15 +1,15 @@ -{ lib -, stdenv -, mkMesonLibrary +{ + lib, + mkMesonLibrary, -, nix-util -, nix-util-c + nix-util, + nix-util-c, -, rapidcheck + rapidcheck, -# Configuration Options + # Configuration Options -, version + version, }: let @@ -22,12 +22,13 @@ mkMesonLibrary (finalAttrs: { workDir = ./.; fileset = fileset.unions [ - ../../build-utils-meson - ./build-utils-meson + ../../nix-meson-build-support + ./nix-meson-build-support ../../.version ./.version ./meson.build # ./meson.options + ./include/nix/util/tests/meson.build (fileset.fileFilter (file: file.hasExt "cc") ./.) (fileset.fileFilter (file: file.hasExt "hh") ./.) ]; @@ -38,21 +39,9 @@ mkMesonLibrary (finalAttrs: { rapidcheck ]; - preConfigure = - # "Inline" .version so it's not a symlink, and includes the suffix. - # Do the meson utils, without modification. - '' - chmod u+w ./.version - echo ${version} > ../../.version - ''; - mesonFlags = [ ]; - env = lib.optionalAttrs (stdenv.isLinux && !(stdenv.hostPlatform.isStatic && stdenv.system == "aarch64-linux")) { - LDFLAGS = "-fuse-ld=gold"; - }; - meta = { platforms = lib.platforms.unix ++ lib.platforms.windows; }; diff --git a/src/libutil-test-support/tests/string_callback.cc b/src/libutil-test-support/string_callback.cc similarity index 83% rename from src/libutil-test-support/tests/string_callback.cc rename to src/libutil-test-support/string_callback.cc index 7a13bd4ff9c..4f6a9cf40fd 100644 --- a/src/libutil-test-support/tests/string_callback.cc +++ b/src/libutil-test-support/string_callback.cc @@ -1,4 +1,4 @@ -#include "string_callback.hh" +#include "nix/util/tests/string_callback.hh" namespace nix::testing { diff --git a/src/libutil-test-support/tests/tracing-file-system-object-sink.cc b/src/libutil-test-support/tests/tracing-file-system-object-sink.cc deleted file mode 100644 index 122a09dcb32..00000000000 --- a/src/libutil-test-support/tests/tracing-file-system-object-sink.cc +++ /dev/null @@ -1,34 +0,0 @@ -#include -#include "tracing-file-system-object-sink.hh" - -namespace nix::test { - -void TracingFileSystemObjectSink::createDirectory(const CanonPath & path) -{ - std::cerr << "createDirectory(" << path << ")\n"; - sink.createDirectory(path); -} - -void TracingFileSystemObjectSink::createRegularFile( - const CanonPath & path, std::function fn) -{ - std::cerr << "createRegularFile(" << path << ")\n"; - sink.createRegularFile(path, [&](CreateRegularFileSink & crf) { - // We could wrap this and trace about the chunks of data and such - fn(crf); - }); -} - -void TracingFileSystemObjectSink::createSymlink(const CanonPath & path, const std::string & target) -{ - std::cerr << "createSymlink(" << path << ", target: " << target << ")\n"; - sink.createSymlink(path, target); -} - -void TracingExtendedFileSystemObjectSink::createHardlink(const CanonPath & path, const CanonPath & target) -{ - std::cerr << "createHardlink(" << path << ", target: " << target << ")\n"; - sink.createHardlink(path, target); -} - -} // namespace nix::test diff --git a/src/libutil-test-support/tests/tracing-file-system-object-sink.hh b/src/libutil-test-support/tests/tracing-file-system-object-sink.hh deleted file mode 100644 index 895ac366405..00000000000 --- a/src/libutil-test-support/tests/tracing-file-system-object-sink.hh +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once -#include "fs-sink.hh" - -namespace nix::test { - -/** - * A `FileSystemObjectSink` that traces calls, writing to stderr. - */ -class TracingFileSystemObjectSink : public virtual FileSystemObjectSink -{ - FileSystemObjectSink & sink; -public: - TracingFileSystemObjectSink(FileSystemObjectSink & sink) - : sink(sink) - { - } - - void createDirectory(const CanonPath & path) override; - - void createRegularFile(const CanonPath & path, std::function fn) override; - - void createSymlink(const CanonPath & path, const std::string & target) override; -}; - -/** - * A `ExtendedFileSystemObjectSink` that traces calls, writing to stderr. - */ -class TracingExtendedFileSystemObjectSink : public TracingFileSystemObjectSink, public ExtendedFileSystemObjectSink -{ - ExtendedFileSystemObjectSink & sink; -public: - TracingExtendedFileSystemObjectSink(ExtendedFileSystemObjectSink & sink) - : TracingFileSystemObjectSink(sink) - , sink(sink) - { - } - - void createHardlink(const CanonPath & path, const CanonPath & target) override; -}; - -} diff --git a/src/libutil-tests/args.cc b/src/libutil-tests/args.cc index 95022443006..f5ad43a557d 100644 --- a/src/libutil-tests/args.cc +++ b/src/libutil-tests/args.cc @@ -1,5 +1,5 @@ -#include "args.hh" -#include "fs-sink.hh" +#include "nix/util/args.hh" +#include "nix/util/fs-sink.hh" #include #include @@ -9,7 +9,7 @@ namespace nix { TEST(parseShebangContent, basic) { std::list r = parseShebangContent("hi there"); - ASSERT_EQ(r.size(), 2); + ASSERT_EQ(r.size(), 2u); auto i = r.begin(); ASSERT_EQ(*i++, "hi"); ASSERT_EQ(*i++, "there"); @@ -17,26 +17,26 @@ namespace nix { TEST(parseShebangContent, empty) { std::list r = parseShebangContent(""); - ASSERT_EQ(r.size(), 0); + ASSERT_EQ(r.size(), 0u); } TEST(parseShebangContent, doubleBacktick) { std::list r = parseShebangContent("``\"ain't that nice\"``"); - ASSERT_EQ(r.size(), 1); + ASSERT_EQ(r.size(), 1u); auto i = r.begin(); ASSERT_EQ(*i++, "\"ain't that nice\""); } TEST(parseShebangContent, doubleBacktickEmpty) { std::list r = parseShebangContent("````"); - ASSERT_EQ(r.size(), 1); + ASSERT_EQ(r.size(), 1u); auto i = r.begin(); ASSERT_EQ(*i++, ""); } TEST(parseShebangContent, doubleBacktickMarkdownInlineCode) { std::list r = parseShebangContent("``# I'm markdown section about `coolFunction` ``"); - ASSERT_EQ(r.size(), 1); + ASSERT_EQ(r.size(), 1u); auto i = r.begin(); ASSERT_EQ(*i++, "# I'm markdown section about `coolFunction`"); } @@ -44,49 +44,49 @@ namespace nix { TEST(parseShebangContent, doubleBacktickMarkdownCodeBlockNaive) { std::list r = parseShebangContent("``Example 1\n```nix\na: a\n``` ``"); auto i = r.begin(); - ASSERT_EQ(r.size(), 1); + ASSERT_EQ(r.size(), 1u); ASSERT_EQ(*i++, "Example 1\n``nix\na: a\n``"); } TEST(parseShebangContent, doubleBacktickMarkdownCodeBlockCorrect) { std::list r = parseShebangContent("``Example 1\n````nix\na: a\n```` ``"); auto i = r.begin(); - ASSERT_EQ(r.size(), 1); + ASSERT_EQ(r.size(), 1u); ASSERT_EQ(*i++, "Example 1\n```nix\na: a\n```"); } TEST(parseShebangContent, doubleBacktickMarkdownCodeBlock2) { std::list r = parseShebangContent("``Example 1\n````nix\na: a\n````\nExample 2\n````nix\na: a\n```` ``"); auto i = r.begin(); - ASSERT_EQ(r.size(), 1); + ASSERT_EQ(r.size(), 1u); ASSERT_EQ(*i++, "Example 1\n```nix\na: a\n```\nExample 2\n```nix\na: a\n```"); } TEST(parseShebangContent, singleBacktickInDoubleBacktickQuotes) { std::list r = parseShebangContent("``` ``"); auto i = r.begin(); - ASSERT_EQ(r.size(), 1); + ASSERT_EQ(r.size(), 1u); ASSERT_EQ(*i++, "`"); } TEST(parseShebangContent, singleBacktickAndSpaceInDoubleBacktickQuotes) { std::list r = parseShebangContent("``` ``"); auto i = r.begin(); - ASSERT_EQ(r.size(), 1); + ASSERT_EQ(r.size(), 1u); ASSERT_EQ(*i++, "` "); } TEST(parseShebangContent, doubleBacktickInDoubleBacktickQuotes) { std::list r = parseShebangContent("````` ``"); auto i = r.begin(); - ASSERT_EQ(r.size(), 1); + ASSERT_EQ(r.size(), 1u); ASSERT_EQ(*i++, "``"); } TEST(parseShebangContent, increasingQuotes) { std::list r = parseShebangContent("```` ``` `` ````` `` `````` ``"); auto i = r.begin(); - ASSERT_EQ(r.size(), 4); + ASSERT_EQ(r.size(), 4u); ASSERT_EQ(*i++, ""); ASSERT_EQ(*i++, "`"); ASSERT_EQ(*i++, "``"); @@ -146,7 +146,7 @@ RC_GTEST_PROP( auto escaped = escape(orig); // RC_LOG() << "escaped: <[[" << escaped << "]]>" << std::endl; auto ss = parseShebangContent(escaped); - RC_ASSERT(ss.size() == 1); + RC_ASSERT(ss.size() == 1u); RC_ASSERT(*ss.begin() == orig); } @@ -156,7 +156,7 @@ RC_GTEST_PROP( (const std::string & one, const std::string & two)) { auto ss = parseShebangContent(escape(one) + " " + escape(two)); - RC_ASSERT(ss.size() == 2); + RC_ASSERT(ss.size() == 2u); auto i = ss.begin(); RC_ASSERT(*i++ == one); RC_ASSERT(*i++ == two); diff --git a/src/libutil-tests/build-utils-meson b/src/libutil-tests/build-utils-meson deleted file mode 120000 index 5fff21bab55..00000000000 --- a/src/libutil-tests/build-utils-meson +++ /dev/null @@ -1 +0,0 @@ -../../build-utils-meson \ No newline at end of file diff --git a/src/libutil-tests/canon-path.cc b/src/libutil-tests/canon-path.cc index 7f91308afe1..c6808bf6673 100644 --- a/src/libutil-tests/canon-path.cc +++ b/src/libutil-tests/canon-path.cc @@ -1,4 +1,4 @@ -#include "canon-path.hh" +#include "nix/util/canon-path.hh" #include diff --git a/src/libutil-tests/checked-arithmetic.cc b/src/libutil-tests/checked-arithmetic.cc index 75018660dc8..8056a430a33 100644 --- a/src/libutil-tests/checked-arithmetic.cc +++ b/src/libutil-tests/checked-arithmetic.cc @@ -5,9 +5,9 @@ #include #include -#include +#include "nix/util/checked-arithmetic.hh" -#include "tests/gtest-with-params.hh" +#include "nix/util/tests/gtest-with-params.hh" namespace rc { using namespace nix; diff --git a/src/libutil-tests/chunked-vector.cc b/src/libutil-tests/chunked-vector.cc index 868d11f6f37..c4f1d385877 100644 --- a/src/libutil-tests/chunked-vector.cc +++ b/src/libutil-tests/chunked-vector.cc @@ -1,16 +1,16 @@ -#include "chunked-vector.hh" +#include "nix/util/chunked-vector.hh" #include namespace nix { TEST(ChunkedVector, InitEmpty) { auto v = ChunkedVector(100); - ASSERT_EQ(v.size(), 0); + ASSERT_EQ(v.size(), 0u); } TEST(ChunkedVector, GrowsCorrectly) { auto v = ChunkedVector(100); - for (auto i = 1; i < 20; i++) { + for (uint32_t i = 1; i < 20; i++) { v.add(i); ASSERT_EQ(v.size(), i); } @@ -31,7 +31,7 @@ namespace nix { for (auto i = 1; i < 20; i++) { v.add(i); } - int count = 0; + uint32_t count = 0; v.forEach([&count](int elt) { count++; }); diff --git a/src/libutil-tests/closure.cc b/src/libutil-tests/closure.cc index 7597e78073b..6bbc128c24e 100644 --- a/src/libutil-tests/closure.cc +++ b/src/libutil-tests/closure.cc @@ -1,4 +1,4 @@ -#include "closure.hh" +#include "nix/util/closure.hh" #include namespace nix { diff --git a/src/libutil-tests/compression.cc b/src/libutil-tests/compression.cc index bbbf3500fbf..de0c7cdb653 100644 --- a/src/libutil-tests/compression.cc +++ b/src/libutil-tests/compression.cc @@ -1,4 +1,4 @@ -#include "compression.hh" +#include "nix/util/compression.hh" #include namespace nix { diff --git a/src/libutil-tests/config.cc b/src/libutil-tests/config.cc index 886e70da50d..bc7db251b87 100644 --- a/src/libutil-tests/config.cc +++ b/src/libutil-tests/config.cc @@ -1,5 +1,5 @@ -#include "config.hh" -#include "args.hh" +#include "nix/util/configuration.hh" +#include "nix/util/args.hh" #include #include diff --git a/src/libutil-tests/executable-path.cc b/src/libutil-tests/executable-path.cc index 8d182357dab..7229b14e6b3 100644 --- a/src/libutil-tests/executable-path.cc +++ b/src/libutil-tests/executable-path.cc @@ -1,6 +1,6 @@ #include -#include "executable-path.hh" +#include "nix/util/executable-path.hh" namespace nix { diff --git a/src/libutil-tests/file-content-address.cc b/src/libutil-tests/file-content-address.cc index 27d926a8736..92c6059a499 100644 --- a/src/libutil-tests/file-content-address.cc +++ b/src/libutil-tests/file-content-address.cc @@ -1,6 +1,7 @@ +#include #include -#include "file-content-address.hh" +#include "nix/util/file-content-address.hh" namespace nix { @@ -26,8 +27,11 @@ TEST(FileSerialisationMethod, testRoundTripPrintParse_2) { } } -TEST(FileSerialisationMethod, testParseFileSerialisationMethodOptException) { - EXPECT_THROW(parseFileSerialisationMethod("narwhal"), UsageError); +TEST(FileSerialisationMethod, testParseFileSerialisationMethodOptException) +{ + EXPECT_THAT( + []() { parseFileSerialisationMethod("narwhal"); }, + testing::ThrowsMessage(testing::HasSubstr("narwhal"))); } /* ---------------------------------------------------------------------------- @@ -54,8 +58,11 @@ TEST(FileIngestionMethod, testRoundTripPrintParse_2) { } } -TEST(FileIngestionMethod, testParseFileIngestionMethodOptException) { - EXPECT_THROW(parseFileIngestionMethod("narwhal"), UsageError); +TEST(FileIngestionMethod, testParseFileIngestionMethodOptException) +{ + EXPECT_THAT( + []() { parseFileIngestionMethod("narwhal"); }, + testing::ThrowsMessage(testing::HasSubstr("narwhal"))); } } diff --git a/src/libutil-tests/file-system.cc b/src/libutil-tests/file-system.cc index 7ef804f3403..2d1058c4ff4 100644 --- a/src/libutil-tests/file-system.cc +++ b/src/libutil-tests/file-system.cc @@ -1,9 +1,9 @@ -#include "util.hh" -#include "types.hh" -#include "file-system.hh" -#include "processes.hh" -#include "terminal.hh" -#include "strings.hh" +#include "nix/util/util.hh" +#include "nix/util/types.hh" +#include "nix/util/file-system.hh" +#include "nix/util/processes.hh" +#include "nix/util/terminal.hh" +#include "nix/util/strings.hh" #include #include @@ -203,12 +203,10 @@ TEST(isInDir, notInDir) ASSERT_EQ(p1, false); } -// XXX: hm, bug or feature? :) Looking at the implementation -// this might be problematic. TEST(isInDir, emptyDir) { auto p1 = isInDir("/zes/foo/bar", ""); - ASSERT_EQ(p1, true); + ASSERT_EQ(p1, false); } /* ---------------------------------------------------------------------------- @@ -233,14 +231,12 @@ TEST(isDirOrInDir, falseForDisjunctPaths) TEST(isDirOrInDir, relativePaths) { - ASSERT_EQ(isDirOrInDir("/foo/..", "/foo"), true); + ASSERT_EQ(isDirOrInDir("/foo/..", "/foo"), false); } -// XXX: while it is possible to use "." or ".." in the -// first argument this doesn't seem to work in the second. -TEST(isDirOrInDir, DISABLED_shouldWork) +TEST(isDirOrInDir, relativePathsTwice) { - ASSERT_EQ(isDirOrInDir("/foo/..", "/foo/."), true); + ASSERT_EQ(isDirOrInDir("/foo/..", "/foo/."), false); } /* ---------------------------------------------------------------------------- @@ -261,4 +257,65 @@ TEST(pathExists, bogusPathDoesNotExist) { ASSERT_FALSE(pathExists("/schnitzel/darmstadt/pommes")); } + +/* ---------------------------------------------------------------------------- + * makeParentCanonical + * --------------------------------------------------------------------------*/ + +TEST(makeParentCanonical, noParent) +{ + ASSERT_EQ(makeParentCanonical("file"), absPath(std::filesystem::path("file"))); +} + +TEST(makeParentCanonical, root) +{ + ASSERT_EQ(makeParentCanonical("/"), "/"); +} + +/* ---------------------------------------------------------------------------- + * chmodIfNeeded + * --------------------------------------------------------------------------*/ + +TEST(chmodIfNeeded, works) +{ + auto [autoClose_, tmpFile] = nix::createTempFile(); + auto deleteTmpFile = AutoDelete(tmpFile); + + auto modes = std::vector{0755, 0644, 0422, 0600, 0777}; + for (mode_t oldMode : modes) { + for (mode_t newMode : modes) { + chmod(tmpFile.c_str(), oldMode); + bool permissionsChanged = false; + ASSERT_NO_THROW(permissionsChanged = chmodIfNeeded(tmpFile, newMode)); + ASSERT_EQ(permissionsChanged, oldMode != newMode); + } + } +} + +TEST(chmodIfNeeded, nonexistent) +{ + ASSERT_THROW(chmodIfNeeded("/schnitzel/darmstadt/pommes", 0755), SysError); +} + +/* ---------------------------------------------------------------------------- + * DirectoryIterator + * --------------------------------------------------------------------------*/ + +TEST(DirectoryIterator, works) +{ + auto tmpDir = nix::createTempDir(); + nix::AutoDelete delTmpDir(tmpDir, true); + + nix::writeFile(tmpDir + "/somefile", ""); + + for (auto path : DirectoryIterator(tmpDir)) { + ASSERT_EQ(path.path().string(), tmpDir + "/somefile"); + } +} + +TEST(DirectoryIterator, nonexistent) +{ + ASSERT_THROW(DirectoryIterator("/schnitzel/darmstadt/pommes"), SysError); +} + } diff --git a/src/libutil-tests/git.cc b/src/libutil-tests/git.cc index 048956a580a..91432b76bcb 100644 --- a/src/libutil-tests/git.cc +++ b/src/libutil-tests/git.cc @@ -1,9 +1,9 @@ #include -#include "git.hh" -#include "memory-source-accessor.hh" +#include "nix/util/git.hh" +#include "nix/util/memory-source-accessor.hh" -#include "tests/characterization.hh" +#include "nix/util/tests/characterization.hh" namespace nix { diff --git a/src/libutil-tests/hash.cc b/src/libutil-tests/hash.cc index a88994d0bc8..3c71b04864f 100644 --- a/src/libutil-tests/hash.cc +++ b/src/libutil-tests/hash.cc @@ -2,14 +2,56 @@ #include -#include "hash.hh" +#include "nix/util/hash.hh" namespace nix { +class BLAKE3HashTest : public virtual ::testing::Test +{ +public: + + /** + * We set these in tests rather than the regular globals so we don't have + * to worry about race conditions if the tests run concurrently. + */ + ExperimentalFeatureSettings mockXpSettings; + +private: + + void SetUp() override + { + mockXpSettings.set("experimental-features", "blake3-hashes"); + } +}; + /* ---------------------------------------------------------------------------- * hashString * --------------------------------------------------------------------------*/ + TEST_F(BLAKE3HashTest, testKnownBLAKE3Hashes1) { + // values taken from: https://tools.ietf.org/html/rfc4634 + auto s = "abc"; + auto hash = hashString(HashAlgorithm::BLAKE3, s, mockXpSettings); + ASSERT_EQ(hash.to_string(HashFormat::Base16, true), + "blake3:6437b3ac38465133ffb63b75273a8db548c558465d79db03fd359c6cd5bd9d85"); + } + + TEST_F(BLAKE3HashTest, testKnownBLAKE3Hashes2) { + // values taken from: https://tools.ietf.org/html/rfc4634 + auto s = "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"; + auto hash = hashString(HashAlgorithm::BLAKE3, s, mockXpSettings); + ASSERT_EQ(hash.to_string(HashFormat::Base16, true), + "blake3:c19012cc2aaf0dc3d8e5c45a1b79114d2df42abb2a410bf54be09e891af06ff8"); + } + + TEST_F(BLAKE3HashTest, testKnownBLAKE3Hashes3) { + // values taken from: https://www.ietf.org/archive/id/draft-aumasson-blake3-00.txt + auto s = "IETF"; + auto hash = hashString(HashAlgorithm::BLAKE3, s, mockXpSettings); + ASSERT_EQ(hash.to_string(HashFormat::Base16, true), + "blake3:83a2de1ee6f4e6ab686889248f4ec0cf4cc5709446a682ffd1cbb4d6165181e2"); + } + TEST(hashString, testKnownMD5Hashes1) { // values taken from: https://tools.ietf.org/html/rfc1321 auto s1 = ""; diff --git a/src/libutil-tests/hilite.cc b/src/libutil-tests/hilite.cc index 5ef58188884..98773afcf58 100644 --- a/src/libutil-tests/hilite.cc +++ b/src/libutil-tests/hilite.cc @@ -1,4 +1,4 @@ -#include "hilite.hh" +#include "nix/util/hilite.hh" #include diff --git a/src/libutil-tests/json-utils.cc b/src/libutil-tests/json-utils.cc index 704a4acb05d..211f8bf1ee4 100644 --- a/src/libutil-tests/json-utils.cc +++ b/src/libutil-tests/json-utils.cc @@ -3,8 +3,8 @@ #include -#include "error.hh" -#include "json-utils.hh" +#include "nix/util/error.hh" +#include "nix/util/json-utils.hh" namespace nix { @@ -50,7 +50,7 @@ TEST(from_json, optionalInt) { TEST(from_json, vectorOfOptionalInts) { nlohmann::json json = { 420, nullptr }; std::vector> vals = json; - ASSERT_EQ(vals.size(), 2); + ASSERT_EQ(vals.size(), 2u); ASSERT_TRUE(vals.at(0).has_value()); ASSERT_EQ(*vals.at(0), 420); ASSERT_FALSE(vals.at(1).has_value()); @@ -63,9 +63,7 @@ TEST(valueAt, simpleObject) { auto nested = R"({ "hello": { "world": "" } })"_json; - auto & nestedObject = valueAt(getObject(nested), "hello"); - - ASSERT_EQ(valueAt(nestedObject, "world"), ""); + ASSERT_EQ(valueAt(valueAt(getObject(nested), "hello"), "world"), ""); } TEST(valueAt, missingKey) { @@ -83,7 +81,7 @@ TEST(getObject, rightAssertions) { auto nested = R"({ "object": { "object": {} } })"_json; - auto & nestedObject = getObject(valueAt(getObject(nested), "object")); + auto nestedObject = getObject(valueAt(getObject(nested), "object")); ASSERT_EQ(nestedObject, getObject(nlohmann::json::parse(R"({ "object": {} })"))); ASSERT_EQ(getObject(valueAt(getObject(nestedObject), "object")), (nlohmann::json::object_t {})); @@ -130,19 +128,29 @@ TEST(getString, wrongAssertions) { ASSERT_THROW(getString(valueAt(json, "boolean")), Error); } -TEST(getInteger, rightAssertions) { - auto simple = R"({ "int": 0 })"_json; +TEST(getIntegralNumber, rightAssertions) { + auto simple = R"({ "int": 0, "signed": -1 })"_json; - ASSERT_EQ(getInteger(valueAt(getObject(simple), "int")), 0); + ASSERT_EQ(getUnsigned(valueAt(getObject(simple), "int")), 0u); + ASSERT_EQ(getInteger(valueAt(getObject(simple), "int")), 0); + ASSERT_EQ(getInteger(valueAt(getObject(simple), "signed")), -1); } -TEST(getInteger, wrongAssertions) { - auto json = R"({ "object": {}, "array": [], "string": "", "int": 0, "boolean": false })"_json; +TEST(getIntegralNumber, wrongAssertions) { + auto json = R"({ "object": {}, "array": [], "string": "", "int": 0, "signed": -256, "large": 128, "boolean": false })"_json; + + ASSERT_THROW(getUnsigned(valueAt(json, "object")), Error); + ASSERT_THROW(getUnsigned(valueAt(json, "array")), Error); + ASSERT_THROW(getUnsigned(valueAt(json, "string")), Error); + ASSERT_THROW(getUnsigned(valueAt(json, "boolean")), Error); + ASSERT_THROW(getUnsigned(valueAt(json, "signed")), Error); - ASSERT_THROW(getInteger(valueAt(json, "object")), Error); - ASSERT_THROW(getInteger(valueAt(json, "array")), Error); - ASSERT_THROW(getInteger(valueAt(json, "string")), Error); - ASSERT_THROW(getInteger(valueAt(json, "boolean")), Error); + ASSERT_THROW(getInteger(valueAt(json, "object")), Error); + ASSERT_THROW(getInteger(valueAt(json, "array")), Error); + ASSERT_THROW(getInteger(valueAt(json, "string")), Error); + ASSERT_THROW(getInteger(valueAt(json, "boolean")), Error); + ASSERT_THROW(getInteger(valueAt(json, "large")), Error); + ASSERT_THROW(getInteger(valueAt(json, "signed")), Error); } TEST(getBoolean, rightAssertions) { diff --git a/src/libutil-tests/logging.cc b/src/libutil-tests/logging.cc index 1d7304f0591..5c9fcfe8f83 100644 --- a/src/libutil-tests/logging.cc +++ b/src/libutil-tests/logging.cc @@ -1,7 +1,7 @@ #if 0 -#include "logging.hh" -#include "nixexpr.hh" +#include "nix/util/logging.hh" +#include "nix/expr/nixexpr.hh" #include #include @@ -19,7 +19,7 @@ namespace nix { const char *one_liner = "this is the other problem line of code"; - TEST(logEI, catpuresBasicProperties) { + TEST(logEI, capturesBasicProperties) { MakeError(TestError, Error); ErrorInfo::programName = std::optional("error-unit-test"); diff --git a/src/libutil-tests/lru-cache.cc b/src/libutil-tests/lru-cache.cc index 091d3d5ede1..a6a27cd3eaa 100644 --- a/src/libutil-tests/lru-cache.cc +++ b/src/libutil-tests/lru-cache.cc @@ -1,4 +1,4 @@ -#include "lru-cache.hh" +#include "nix/util/lru-cache.hh" #include namespace nix { @@ -9,13 +9,13 @@ namespace nix { TEST(LRUCache, sizeOfEmptyCacheIsZero) { LRUCache c(10); - ASSERT_EQ(c.size(), 0); + ASSERT_EQ(c.size(), 0u); } TEST(LRUCache, sizeOfSingleElementCacheIsOne) { LRUCache c(10); c.upsert("foo", "bar"); - ASSERT_EQ(c.size(), 1); + ASSERT_EQ(c.size(), 1u); } /* ---------------------------------------------------------------------------- @@ -55,12 +55,12 @@ namespace nix { auto val = c.get("foo"); ASSERT_EQ(val.value_or("error"), "bar"); - ASSERT_EQ(c.size(), 1); + ASSERT_EQ(c.size(), 1u); c.upsert("foo", "changed"); val = c.get("foo"); ASSERT_EQ(val.value_or("error"), "changed"); - ASSERT_EQ(c.size(), 1); + ASSERT_EQ(c.size(), 1u); } TEST(LRUCache, overwriteOldestWhenCapacityIsReached) { @@ -69,13 +69,13 @@ namespace nix { c.upsert("two", "zwei"); c.upsert("three", "drei"); - ASSERT_EQ(c.size(), 3); + ASSERT_EQ(c.size(), 3u); ASSERT_EQ(c.get("one").value_or("error"), "eins"); // exceed capacity c.upsert("another", "whatever"); - ASSERT_EQ(c.size(), 3); + ASSERT_EQ(c.size(), 3u); // Retrieving "one" makes it the most recent element thus // two will be the oldest one and thus replaced. ASSERT_EQ(c.get("two").has_value(), false); @@ -89,7 +89,7 @@ namespace nix { TEST(LRUCache, clearEmptyCache) { LRUCache c(10); c.clear(); - ASSERT_EQ(c.size(), 0); + ASSERT_EQ(c.size(), 0u); } TEST(LRUCache, clearNonEmptyCache) { @@ -97,9 +97,9 @@ namespace nix { c.upsert("one", "eins"); c.upsert("two", "zwei"); c.upsert("three", "drei"); - ASSERT_EQ(c.size(), 3); + ASSERT_EQ(c.size(), 3u); c.clear(); - ASSERT_EQ(c.size(), 0); + ASSERT_EQ(c.size(), 0u); } /* ---------------------------------------------------------------------------- @@ -109,14 +109,14 @@ namespace nix { TEST(LRUCache, eraseFromEmptyCache) { LRUCache c(10); ASSERT_EQ(c.erase("foo"), false); - ASSERT_EQ(c.size(), 0); + ASSERT_EQ(c.size(), 0u); } TEST(LRUCache, eraseMissingFromNonEmptyCache) { LRUCache c(10); c.upsert("one", "eins"); ASSERT_EQ(c.erase("foo"), false); - ASSERT_EQ(c.size(), 1); + ASSERT_EQ(c.size(), 1u); ASSERT_EQ(c.get("one").value_or("error"), "eins"); } @@ -124,7 +124,7 @@ namespace nix { LRUCache c(10); c.upsert("one", "eins"); ASSERT_EQ(c.erase("one"), true); - ASSERT_EQ(c.size(), 0); + ASSERT_EQ(c.size(), 0u); ASSERT_EQ(c.get("one").value_or("empty"), "empty"); } } diff --git a/src/libutil-tests/meson.build b/src/libutil-tests/meson.build index f593507742b..b3776e0940a 100644 --- a/src/libutil-tests/meson.build +++ b/src/libutil-tests/meson.build @@ -4,8 +4,6 @@ project('nix-util-tests', 'cpp', 'cpp_std=c++2a', # TODO(Qyriad): increase the warning level 'warning_level=1', - 'debug=true', - 'optimization=2', 'errorlogs=true', # Please print logs for tests that fail ], meson_version : '>= 1.1', @@ -14,7 +12,7 @@ project('nix-util-tests', 'cpp', cxx = meson.get_compiler('cpp') -subdir('build-utils-meson/deps-lists') +subdir('nix-meson-build-support/deps-lists') deps_private_maybe_subproject = [ dependency('nix-util'), @@ -23,10 +21,10 @@ deps_private_maybe_subproject = [ ] deps_public_maybe_subproject = [ ] -subdir('build-utils-meson/subprojects') +subdir('nix-meson-build-support/subprojects') -subdir('build-utils-meson/export-all-symbols') -subdir('build-utils-meson/windows-version') +subdir('nix-meson-build-support/export-all-symbols') +subdir('nix-meson-build-support/windows-version') rapidcheck = dependency('rapidcheck') deps_private += rapidcheck @@ -34,15 +32,15 @@ deps_private += rapidcheck gtest = dependency('gtest', main : true) deps_private += gtest -add_project_arguments( - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. - '-include', 'config-util.hh', - '-include', 'config-util.h', - language : 'cpp', +configdata = configuration_data() +configdata.set_quoted('PACKAGE_VERSION', meson.project_version()) + +config_priv_h = configure_file( + configuration : configdata, + output : 'util-tests-config.hh', ) -subdir('build-utils-meson/common') +subdir('nix-meson-build-support/common') sources = files( 'args.cc', @@ -61,11 +59,13 @@ sources = files( 'json-utils.cc', 'logging.cc', 'lru-cache.cc', + 'monitorfdhup.cc', 'nix_api_util.cc', 'pool.cc', 'position.cc', 'processes.cc', 'references.cc', + 'sort.cc', 'spawn.cc', 'strings.cc', 'suggestions.cc', @@ -81,6 +81,7 @@ include_dirs = [include_directories('.')] this_exe = executable( meson.project_name(), sources, + config_priv_h, dependencies : deps_private_subproject + deps_private + deps_other, include_directories : include_dirs, # TODO: -lrapidcheck, see ../libutil-support/build.meson diff --git a/src/libutil-tests/monitorfdhup.cc b/src/libutil-tests/monitorfdhup.cc new file mode 100644 index 00000000000..8e6fed6f07c --- /dev/null +++ b/src/libutil-tests/monitorfdhup.cc @@ -0,0 +1,22 @@ +#ifndef _WIN32 + +# include "nix/util/util.hh" +# include "nix/util/monitor-fd.hh" + +# include +# include + +namespace nix { +TEST(MonitorFdHup, shouldNotBlock) +{ + Pipe p; + p.create(); + { + // when monitor gets destroyed it should cancel the + // background thread and do not block + MonitorFdHup monitor(p.readSide.get()); + } +} +} + +#endif diff --git a/src/libutil-tests/nix-meson-build-support b/src/libutil-tests/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/libutil-tests/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/libutil-tests/nix_api_util.cc b/src/libutil-tests/nix_api_util.cc index 7b77bd87fac..baaaa81fc3a 100644 --- a/src/libutil-tests/nix_api_util.cc +++ b/src/libutil-tests/nix_api_util.cc @@ -1,14 +1,16 @@ -#include "config-global.hh" -#include "args.hh" +#include "nix/util/config-global.hh" +#include "nix/util/args.hh" #include "nix_api_util.h" #include "nix_api_util_internal.h" -#include "tests/nix_api_util.hh" -#include "tests/string_callback.hh" +#include "nix/util/tests/nix_api_util.hh" +#include "nix/util/tests/string_callback.hh" #include #include +#include "util-tests-config.hh" + namespace nixC { TEST_F(nix_api_util_context, nix_context_error) diff --git a/src/libutil-tests/package.nix b/src/libutil-tests/package.nix index b099037eee3..c06de6894af 100644 --- a/src/libutil-tests/package.nix +++ b/src/libutil-tests/package.nix @@ -1,19 +1,20 @@ -{ lib -, buildPackages -, stdenv -, mkMesonExecutable +{ + lib, + buildPackages, + stdenv, + mkMesonExecutable, -, nix-util -, nix-util-c -, nix-util-test-support + nix-util, + nix-util-c, + nix-util-test-support, -, rapidcheck -, gtest -, runCommand + rapidcheck, + gtest, + runCommand, -# Configuration Options + # Configuration Options -, version + version, }: let @@ -26,8 +27,8 @@ mkMesonExecutable (finalAttrs: { workDir = ./.; fileset = fileset.unions [ - ../../build-utils-meson - ./build-utils-meson + ../../nix-meson-build-support + ./nix-meson-build-support ../../.version ./.version ./meson.build @@ -44,33 +45,27 @@ mkMesonExecutable (finalAttrs: { gtest ]; - preConfigure = - # "Inline" .version so it's not a symlink, and includes the suffix. - # Do the meson utils, without modification. - '' - chmod u+w ./.version - echo ${version} > ../../.version - ''; - mesonFlags = [ ]; - env = lib.optionalAttrs (stdenv.isLinux && !(stdenv.hostPlatform.isStatic && stdenv.system == "aarch64-linux")) { - LDFLAGS = "-fuse-ld=gold"; - }; - passthru = { tests = { - run = runCommand "${finalAttrs.pname}-run" { - meta.broken = !stdenv.hostPlatform.emulatorAvailable buildPackages; - } (lib.optionalString stdenv.hostPlatform.isWindows '' - export HOME="$PWD/home-dir" - mkdir -p "$HOME" - '' + '' - export _NIX_TEST_UNIT_DATA=${./data} - ${stdenv.hostPlatform.emulator buildPackages} ${lib.getExe finalAttrs.finalPackage} - touch $out - ''); + run = + runCommand "${finalAttrs.pname}-run" + { + meta.broken = !stdenv.hostPlatform.emulatorAvailable buildPackages; + } + ( + lib.optionalString stdenv.hostPlatform.isWindows '' + export HOME="$PWD/home-dir" + mkdir -p "$HOME" + '' + + '' + export _NIX_TEST_UNIT_DATA=${./data} + ${stdenv.hostPlatform.emulator buildPackages} ${lib.getExe finalAttrs.finalPackage} + touch $out + '' + ); }; }; diff --git a/src/libutil-tests/pool.cc b/src/libutil-tests/pool.cc index 127e42dda2b..d41bab8ed8b 100644 --- a/src/libutil-tests/pool.cc +++ b/src/libutil-tests/pool.cc @@ -1,4 +1,4 @@ -#include "pool.hh" +#include "nix/util/pool.hh" #include namespace nix { @@ -26,8 +26,8 @@ namespace nix { Pool pool = Pool((size_t)1, createResource, isGood); - ASSERT_EQ(pool.count(), 0); - ASSERT_EQ(pool.capacity(), 1); + ASSERT_EQ(pool.count(), 0u); + ASSERT_EQ(pool.capacity(), 1u); } TEST(Pool, freshPoolCanGetAResource) { @@ -35,12 +35,12 @@ namespace nix { auto createResource = []() { return make_ref(); }; Pool pool = Pool((size_t)1, createResource, isGood); - ASSERT_EQ(pool.count(), 0); + ASSERT_EQ(pool.count(), 0u); TestResource r = *(pool.get()); - ASSERT_EQ(pool.count(), 1); - ASSERT_EQ(pool.capacity(), 1); + ASSERT_EQ(pool.count(), 1u); + ASSERT_EQ(pool.capacity(), 1u); ASSERT_EQ(r.dummyValue, 1); ASSERT_EQ(r.good, true); } @@ -50,9 +50,9 @@ namespace nix { auto createResource = []() { return make_ref(); }; Pool pool = Pool((size_t)1, createResource, isGood); - ASSERT_EQ(pool.capacity(), 1); + ASSERT_EQ(pool.capacity(), 1u); pool.incCapacity(); - ASSERT_EQ(pool.capacity(), 2); + ASSERT_EQ(pool.capacity(), 2u); } TEST(Pool, capacityCanBeDecremented) { @@ -60,9 +60,9 @@ namespace nix { auto createResource = []() { return make_ref(); }; Pool pool = Pool((size_t)1, createResource, isGood); - ASSERT_EQ(pool.capacity(), 1); + ASSERT_EQ(pool.capacity(), 1u); pool.decCapacity(); - ASSERT_EQ(pool.capacity(), 0); + ASSERT_EQ(pool.capacity(), 0u); } TEST(Pool, flushBadDropsOutOfScopeResources) { @@ -73,11 +73,11 @@ namespace nix { { auto _r = pool.get(); - ASSERT_EQ(pool.count(), 1); + ASSERT_EQ(pool.count(), 1u); } pool.flushBad(); - ASSERT_EQ(pool.count(), 0); + ASSERT_EQ(pool.count(), 0u); } // Test that the resources we allocate are being reused when they are still good. diff --git a/src/libutil-tests/position.cc b/src/libutil-tests/position.cc index 484ecc2479b..fd65acd039c 100644 --- a/src/libutil-tests/position.cc +++ b/src/libutil-tests/position.cc @@ -1,6 +1,6 @@ #include -#include "position.hh" +#include "nix/util/position.hh" namespace nix { diff --git a/src/libutil-tests/processes.cc b/src/libutil-tests/processes.cc index 9033595e85c..eb7561393ce 100644 --- a/src/libutil-tests/processes.cc +++ b/src/libutil-tests/processes.cc @@ -1,4 +1,4 @@ -#include "processes.hh" +#include "nix/util/processes.hh" #include diff --git a/src/libutil-tests/references.cc b/src/libutil-tests/references.cc index c3efa6d5101..622b3c35a43 100644 --- a/src/libutil-tests/references.cc +++ b/src/libutil-tests/references.cc @@ -1,4 +1,4 @@ -#include "references.hh" +#include "nix/util/references.hh" #include namespace nix { diff --git a/src/libutil-tests/sort.cc b/src/libutil-tests/sort.cc new file mode 100644 index 00000000000..8eee961c8cd --- /dev/null +++ b/src/libutil-tests/sort.cc @@ -0,0 +1,274 @@ +#include +#include +#include "nix/util/sort.hh" + +#include +#include +#include +#include + +namespace nix { + +struct MonotonicSubranges : public ::testing::Test +{ + std::vector empty_; + std::vector basic_ = {1, 0, -1, -100, 10, 10, 20, 40, 5, 5, 20, 10, 10, 1, -5}; +}; + +TEST_F(MonotonicSubranges, empty) +{ + ASSERT_EQ(weaklyIncreasingPrefix(empty_.begin(), empty_.end()), empty_.begin()); + ASSERT_EQ(weaklyIncreasingSuffix(empty_.begin(), empty_.end()), empty_.begin()); + ASSERT_EQ(strictlyDecreasingPrefix(empty_.begin(), empty_.end()), empty_.begin()); + ASSERT_EQ(strictlyDecreasingSuffix(empty_.begin(), empty_.end()), empty_.begin()); +} + +TEST_F(MonotonicSubranges, basic) +{ + ASSERT_EQ(strictlyDecreasingPrefix(basic_.begin(), basic_.end()), basic_.begin() + 4); + ASSERT_EQ(strictlyDecreasingSuffix(basic_.begin(), basic_.end()), basic_.begin() + 12); + std::reverse(basic_.begin(), basic_.end()); + ASSERT_EQ(weaklyIncreasingPrefix(basic_.begin(), basic_.end()), basic_.begin() + 5); + ASSERT_EQ(weaklyIncreasingSuffix(basic_.begin(), basic_.end()), basic_.begin() + 11); +} + +template +class SortTestPermutations : public ::testing::Test +{ + std::vector initialData = {std::numeric_limits::max(), std::numeric_limits::min(), 0, 0, 42, 126, 36}; + std::vector vectorData; + std::list listData; + +public: + std::vector scratchVector; + std::list scratchList; + std::vector empty; + + void SetUp() override + { + vectorData = initialData; + std::sort(vectorData.begin(), vectorData.end()); + listData = std::list(vectorData.begin(), vectorData.end()); + } + + bool nextPermutation() + { + std::next_permutation(vectorData.begin(), vectorData.end()); + std::next_permutation(listData.begin(), listData.end()); + scratchList = listData; + scratchVector = vectorData; + return vectorData == initialData; + } +}; + +using SortPermutationsTypes = ::testing::Types; + +TYPED_TEST_SUITE(SortTestPermutations, SortPermutationsTypes); + +TYPED_TEST(SortTestPermutations, insertionsort) +{ + while (!this->nextPermutation()) { + auto & list = this->scratchList; + insertionsort(list.begin(), list.end()); + ASSERT_TRUE(std::is_sorted(list.begin(), list.end())); + auto & vector = this->scratchVector; + insertionsort(vector.begin(), vector.end()); + ASSERT_TRUE(std::is_sorted(vector.begin(), vector.end())); + } +} + +TYPED_TEST(SortTestPermutations, peeksort) +{ + while (!this->nextPermutation()) { + auto & vector = this->scratchVector; + peeksort(vector.begin(), vector.end()); + ASSERT_TRUE(std::is_sorted(vector.begin(), vector.end())); + } +} + +TEST(InsertionSort, empty) +{ + std::vector empty; + insertionsort(empty.begin(), empty.end()); +} + +struct RandomPeekSort : public ::testing::TestWithParam< + std::tuple> +{ + using ValueType = int; + std::vector data_; + std::mt19937 urng_; + std::uniform_int_distribution distribution_; + + void SetUp() override + { + auto [maxSize, min, max, iterations] = GetParam(); + urng_ = std::mt19937(GTEST_FLAG_GET(random_seed)); + distribution_ = std::uniform_int_distribution(min, max); + } + + auto regenerate() + { + auto [maxSize, min, max, iterations] = GetParam(); + std::size_t dataSize = std::uniform_int_distribution(0, maxSize)(urng_); + data_.resize(dataSize); + std::generate(data_.begin(), data_.end(), [&]() { return distribution_(urng_); }); + } +}; + +TEST_P(RandomPeekSort, defaultComparator) +{ + auto [maxSize, min, max, iterations] = GetParam(); + + for (std::size_t i = 0; i < iterations; ++i) { + regenerate(); + peeksort(data_.begin(), data_.end()); + ASSERT_TRUE(std::is_sorted(data_.begin(), data_.end())); + /* Sorting is idempotent */ + peeksort(data_.begin(), data_.end()); + ASSERT_TRUE(std::is_sorted(data_.begin(), data_.end())); + } +} + +TEST_P(RandomPeekSort, greater) +{ + auto [maxSize, min, max, iterations] = GetParam(); + + for (std::size_t i = 0; i < iterations; ++i) { + regenerate(); + peeksort(data_.begin(), data_.end(), std::greater{}); + ASSERT_TRUE(std::is_sorted(data_.begin(), data_.end(), std::greater{})); + /* Sorting is idempotent */ + peeksort(data_.begin(), data_.end(), std::greater{}); + ASSERT_TRUE(std::is_sorted(data_.begin(), data_.end(), std::greater{})); + } +} + +TEST_P(RandomPeekSort, brokenComparator) +{ + auto [maxSize, min, max, iterations] = GetParam(); + + /* This is a pretty nice way of modeling a worst-case scenario for a broken comparator. + If the sorting algorithm doesn't break in such case, then surely all deterministic + predicates won't break it. */ + auto comp = [&]([[maybe_unused]] const auto & lhs, [[maybe_unused]] const auto & rhs) -> bool { + return std::uniform_int_distribution(0, 1)(urng_); + }; + + for (std::size_t i = 0; i < iterations; ++i) { + regenerate(); + auto originalData = data_; + peeksort(data_.begin(), data_.end(), comp); + /* Check that the output is just a reordering of the input. This is the + contract of the implementation in regard to comparators that don't + define a strict weak order. */ + std::sort(data_.begin(), data_.end()); + std::sort(originalData.begin(), originalData.end()); + ASSERT_EQ(originalData, data_); + } +} + +TEST_P(RandomPeekSort, stability) +{ + auto [maxSize, min, max, iterations] = GetParam(); + + for (std::size_t i = 0; i < iterations; ++i) { + regenerate(); + std::vector> pairs; + + /* Assign sequential ids to objects. After the sort ids for equivalent + elements should be in ascending order. */ + std::transform( + data_.begin(), data_.end(), std::back_inserter(pairs), [id = std::size_t{0}](auto && val) mutable { + return std::pair{val, ++id}; + }); + + auto comp = [&]([[maybe_unused]] const auto & lhs, [[maybe_unused]] const auto & rhs) -> bool { + return lhs.first > rhs.first; + }; + + peeksort(pairs.begin(), pairs.end(), comp); + ASSERT_TRUE(std::is_sorted(pairs.begin(), pairs.end(), comp)); + + for (auto begin = pairs.begin(), end = pairs.end(); begin < end; ++begin) { + auto key = begin->first; + auto innerEnd = std::find_if_not(begin, end, [key](const auto & lhs) { return lhs.first == key; }); + ASSERT_TRUE(std::is_sorted(begin, innerEnd, [](const auto & lhs, const auto & rhs) { + return lhs.second < rhs.second; + })); + begin = innerEnd; + } + } +} + +using RandomPeekSortParamType = RandomPeekSort::ParamType; + +INSTANTIATE_TEST_SUITE_P( + PeekSort, + RandomPeekSort, + ::testing::Values( + RandomPeekSortParamType{128, std::numeric_limits::min(), std::numeric_limits::max(), 1024}, + RandomPeekSortParamType{7753, -32, 32, 128}, + RandomPeekSortParamType{11719, std::numeric_limits::min(), std::numeric_limits::max(), 64}, + RandomPeekSortParamType{4063, 0, 32, 256}, + RandomPeekSortParamType{771, -8, 8, 2048}, + RandomPeekSortParamType{433, 0, 1, 2048}, + RandomPeekSortParamType{0, 0, 0, 1}, /* empty case */ + RandomPeekSortParamType{ + 1, std::numeric_limits::min(), std::numeric_limits::max(), 1}, /* single element */ + RandomPeekSortParamType{ + 2, std::numeric_limits::min(), std::numeric_limits::max(), 2}, /* two elements */ + RandomPeekSortParamType{55425, std::numeric_limits::min(), std::numeric_limits::max(), 128})); + +template +struct SortProperty : public ::testing::Test +{}; + +using SortPropertyTypes = ::testing::Types; +TYPED_TEST_SUITE(SortProperty, SortPropertyTypes); + +RC_GTEST_TYPED_FIXTURE_PROP(SortProperty, peeksortSorted, (std::vector vec)) +{ + peeksort(vec.begin(), vec.end()); + RC_ASSERT(std::is_sorted(vec.begin(), vec.end())); +} + +RC_GTEST_TYPED_FIXTURE_PROP(SortProperty, peeksortSortedGreater, (std::vector vec)) +{ + auto comp = std::greater(); + peeksort(vec.begin(), vec.end(), comp); + RC_ASSERT(std::is_sorted(vec.begin(), vec.end(), comp)); +} + +RC_GTEST_TYPED_FIXTURE_PROP(SortProperty, insertionsortSorted, (std::vector vec)) +{ + insertionsort(vec.begin(), vec.end()); + RC_ASSERT(std::is_sorted(vec.begin(), vec.end())); +} + +RC_GTEST_PROP(SortProperty, peeksortStability, (std::vector> vec)) +{ + auto comp = [](auto lhs, auto rhs) { return lhs.first < rhs.first; }; + auto copy = vec; + std::stable_sort(copy.begin(), copy.end(), comp); + peeksort(vec.begin(), vec.end(), comp); + RC_ASSERT(copy == vec); +} + +RC_GTEST_TYPED_FIXTURE_PROP(SortProperty, peeksortSortedLinearComparisonComplexity, (std::vector vec)) +{ + peeksort(vec.begin(), vec.end()); + RC_ASSERT(std::is_sorted(vec.begin(), vec.end())); + std::size_t comparisonCount = 0; + auto countingComp = [&](auto lhs, auto rhs) { + ++comparisonCount; + return lhs < rhs; + }; + + peeksort(vec.begin(), vec.end(), countingComp); + + /* In the sorted case comparison complexify should be linear. */ + RC_ASSERT(comparisonCount <= vec.size()); +} + +} // namespace nix diff --git a/src/libutil-tests/spawn.cc b/src/libutil-tests/spawn.cc index c617acae08e..594bced592c 100644 --- a/src/libutil-tests/spawn.cc +++ b/src/libutil-tests/spawn.cc @@ -1,6 +1,6 @@ #include -#include "processes.hh" +#include "nix/util/processes.hh" namespace nix { diff --git a/src/libutil-tests/strings.cc b/src/libutil-tests/strings.cc index 8ceb1676760..bf1f66025eb 100644 --- a/src/libutil-tests/strings.cc +++ b/src/libutil-tests/strings.cc @@ -1,12 +1,11 @@ #include #include -#include "strings.hh" +#include "nix/util/strings.hh" +#include "nix/util/error.hh" namespace nix { -using Strings = std::vector; - /* ---------------------------------------------------------------------------- * concatStringsSep * --------------------------------------------------------------------------*/ @@ -81,6 +80,42 @@ TEST(concatStringsSep, buildSingleString) ASSERT_EQ(concatStringsSep(",", strings), "this"); } +TEST(concatMapStringsSep, empty) +{ + Strings strings; + + ASSERT_EQ(concatMapStringsSep(",", strings, [](const std::string & s) { return s; }), ""); +} + +TEST(concatMapStringsSep, justOne) +{ + Strings strings; + strings.push_back("this"); + + ASSERT_EQ(concatMapStringsSep(",", strings, [](const std::string & s) { return s; }), "this"); +} + +TEST(concatMapStringsSep, two) +{ + Strings strings; + strings.push_back("this"); + strings.push_back("that"); + + ASSERT_EQ(concatMapStringsSep(",", strings, [](const std::string & s) { return s; }), "this,that"); +} + +TEST(concatMapStringsSep, map) +{ + StringMap strings; + strings["this"] = "that"; + strings["1"] = "one"; + + ASSERT_EQ( + concatMapStringsSep( + ", ", strings, [](const std::pair & s) { return s.first + " -> " + s.second; }), + "1 -> one, this -> that"); +} + /* ---------------------------------------------------------------------------- * dropEmptyInitThenConcatStringsSep * --------------------------------------------------------------------------*/ @@ -345,4 +380,108 @@ RC_GTEST_PROP(splitString, recoveredByConcatStringsSep, (const std::string & s)) RC_ASSERT(concatStringsSep("a", splitString(s, "a")) == s); } +/* ---------------------------------------------------------------------------- + * shellSplitString + * --------------------------------------------------------------------------*/ + +TEST(shellSplitString, empty) +{ + std::list expected = {}; + + ASSERT_EQ(shellSplitString(""), expected); +} + +TEST(shellSplitString, oneWord) +{ + std::list expected = {"foo"}; + + ASSERT_EQ(shellSplitString("foo"), expected); +} + +TEST(shellSplitString, oneWordQuotedWithSpaces) +{ + std::list expected = {"foo bar"}; + + ASSERT_EQ(shellSplitString("'foo bar'"), expected); +} + +TEST(shellSplitString, oneWordQuotedWithSpacesAndDoubleQuoteInSingleQuote) +{ + std::list expected = {"foo bar\""}; + + ASSERT_EQ(shellSplitString("'foo bar\"'"), expected); +} + +TEST(shellSplitString, oneWordQuotedWithDoubleQuotes) +{ + std::list expected = {"foo bar"}; + + ASSERT_EQ(shellSplitString("\"foo bar\""), expected); +} + +TEST(shellSplitString, twoWords) +{ + std::list expected = {"foo", "bar"}; + + ASSERT_EQ(shellSplitString("foo bar"), expected); +} + +TEST(shellSplitString, twoWordsWithSpacesAndQuotesQuoted) +{ + std::list expected = {"foo bar'", "baz\""}; + + ASSERT_EQ(shellSplitString("\"foo bar'\" 'baz\"'"), expected); +} + +TEST(shellSplitString, emptyArgumentsAreAllowedSingleQuotes) +{ + std::list expected = {"foo", "", "bar", "baz", ""}; + + ASSERT_EQ(shellSplitString("foo '' bar baz ''"), expected); +} + +TEST(shellSplitString, emptyArgumentsAreAllowedDoubleQuotes) +{ + std::list expected = {"foo", "", "bar", "baz", ""}; + + ASSERT_EQ(shellSplitString("foo \"\" bar baz \"\""), expected); +} + +TEST(shellSplitString, singleQuoteDoesNotUseEscapes) +{ + std::list expected = {"foo\\\"bar"}; + + ASSERT_EQ(shellSplitString("'foo\\\"bar'"), expected); +} + +TEST(shellSplitString, doubleQuoteDoesUseEscapes) +{ + std::list expected = {"foo\"bar"}; + + ASSERT_EQ(shellSplitString("\"foo\\\"bar\""), expected); +} + +TEST(shellSplitString, backslashEscapesSpaces) +{ + std::list expected = {"foo bar", "baz", "qux quux"}; + + ASSERT_EQ(shellSplitString("foo\\ bar baz qux\\ quux"), expected); +} + +TEST(shellSplitString, backslashEscapesQuotes) +{ + std::list expected = {"foo\"bar", "baz", "qux'quux"}; + + ASSERT_EQ(shellSplitString("foo\\\"bar baz qux\\'quux"), expected); +} + +TEST(shellSplitString, testUnbalancedQuotes) +{ + ASSERT_THROW(shellSplitString("foo'"), Error); + ASSERT_THROW(shellSplitString("foo\""), Error); + ASSERT_THROW(shellSplitString("foo'bar"), Error); + ASSERT_THROW(shellSplitString("foo\"bar"), Error); + ASSERT_THROW(shellSplitString("foo\"bar\\\""), Error); +} + } // namespace nix diff --git a/src/libutil-tests/suggestions.cc b/src/libutil-tests/suggestions.cc index 279994abc67..d21b286c8fd 100644 --- a/src/libutil-tests/suggestions.cc +++ b/src/libutil-tests/suggestions.cc @@ -1,4 +1,4 @@ -#include "suggestions.hh" +#include "nix/util/suggestions.hh" #include namespace nix { @@ -34,10 +34,10 @@ namespace nix { TEST(Suggestions, Trim) { auto suggestions = Suggestions::bestMatches({"foooo", "bar", "fo", "gao"}, "foo"); auto onlyOne = suggestions.trim(1); - ASSERT_EQ(onlyOne.suggestions.size(), 1); + ASSERT_EQ(onlyOne.suggestions.size(), 1u); ASSERT_TRUE(onlyOne.suggestions.begin()->suggestion == "fo"); auto closest = suggestions.trim(999, 2); - ASSERT_EQ(closest.suggestions.size(), 3); + ASSERT_EQ(closest.suggestions.size(), 3u); } } diff --git a/src/libutil-tests/terminal.cc b/src/libutil-tests/terminal.cc index 714d5a2378c..198b209515e 100644 --- a/src/libutil-tests/terminal.cc +++ b/src/libutil-tests/terminal.cc @@ -1,7 +1,7 @@ -#include "util.hh" -#include "types.hh" -#include "terminal.hh" -#include "strings.hh" +#include "nix/util/util.hh" +#include "nix/util/types.hh" +#include "nix/util/terminal.hh" +#include "nix/util/strings.hh" #include #include @@ -55,6 +55,10 @@ TEST(filterANSIEscapes, utf8) ASSERT_EQ(filterANSIEscapes("fóóbär", true, 3), "fóó"); ASSERT_EQ(filterANSIEscapes("f€€bär", true, 4), "f€€b"); ASSERT_EQ(filterANSIEscapes("fðˆðˆbär", true, 4), "fðˆðˆb"); + ASSERT_EQ(filterANSIEscapes("fðŸ”bar", true, 6), "fðŸ”bar"); + ASSERT_EQ(filterANSIEscapes("fðŸ”bar", true, 3), "fðŸ”"); + ASSERT_EQ(filterANSIEscapes("fðŸ”bar", true, 2), "f"); + ASSERT_EQ(filterANSIEscapes("foo\u0301", true, 3), "fooÌ"); } TEST(filterANSIEscapes, osc8) @@ -62,4 +66,12 @@ TEST(filterANSIEscapes, osc8) ASSERT_EQ(filterANSIEscapes("\e]8;;http://example.com\e\\This is a link\e]8;;\e\\"), "This is a link"); } +TEST(filterANSIEscapes, osc8_bell_as_sep) +{ + // gcc-14 uses \a as a separator, xterm style: + // https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda + ASSERT_EQ(filterANSIEscapes("\e]8;;http://example.com\aThis is a link\e]8;;\a"), "This is a link"); + ASSERT_EQ(filterANSIEscapes("\e]8;;http://example.com\a\\This is a link\e]8;;\a"), "\\This is a link"); +} + } // namespace nix diff --git a/src/libutil-tests/url.cc b/src/libutil-tests/url.cc index 7d08f467eac..c93a96d84b6 100644 --- a/src/libutil-tests/url.cc +++ b/src/libutil-tests/url.cc @@ -1,12 +1,12 @@ -#include "url.hh" +#include "nix/util/url.hh" #include namespace nix { /* ----------- tests for url.hh --------------------------------------------------*/ - std::string print_map(std::map m) { - std::map::iterator it; + std::string print_map(StringMap m) { + StringMap::iterator it; std::string s = "{ "; for (it = m.begin(); it != m.end(); ++it) { s += "{ "; @@ -20,24 +20,11 @@ namespace nix { } - std::ostream& operator<<(std::ostream& os, const ParsedURL& p) { - return os << "\n" - << "url: " << p.url << "\n" - << "base: " << p.base << "\n" - << "scheme: " << p.scheme << "\n" - << "authority: " << p.authority.value() << "\n" - << "path: " << p.path << "\n" - << "query: " << print_map(p.query) << "\n" - << "fragment: " << p.fragment << "\n"; - } - TEST(parseURL, parsesSimpleHttpUrl) { auto s = "http://www.example.org/file.tar.gz"; auto parsed = parseURL(s); ParsedURL expected { - .url = "http://www.example.org/file.tar.gz", - .base = "http://www.example.org/file.tar.gz", .scheme = "http", .authority = "www.example.org", .path = "/file.tar.gz", @@ -53,8 +40,6 @@ namespace nix { auto parsed = parseURL(s); ParsedURL expected { - .url = "https://www.example.org/file.tar.gz", - .base = "https://www.example.org/file.tar.gz", .scheme = "https", .authority = "www.example.org", .path = "/file.tar.gz", @@ -70,8 +55,6 @@ namespace nix { auto parsed = parseURL(s); ParsedURL expected { - .url = "https://www.example.org/file.tar.gz", - .base = "https://www.example.org/file.tar.gz", .scheme = "https", .authority = "www.example.org", .path = "/file.tar.gz", @@ -87,8 +70,6 @@ namespace nix { auto parsed = parseURL(s); ParsedURL expected { - .url = "http://www.example.org/file.tar.gz", - .base = "http://www.example.org/file.tar.gz", .scheme = "http", .authority = "www.example.org", .path = "/file.tar.gz", @@ -104,8 +85,6 @@ namespace nix { auto parsed = parseURL(s); ParsedURL expected { - .url = "file+https://www.example.org/video.mp4", - .base = "https://www.example.org/video.mp4", .scheme = "file+https", .authority = "www.example.org", .path = "/video.mp4", @@ -126,8 +105,6 @@ namespace nix { auto parsed = parseURL(s); ParsedURL expected { - .url = "http://127.0.0.1:8080/file.tar.gz", - .base = "https://127.0.0.1:8080/file.tar.gz", .scheme = "http", .authority = "127.0.0.1:8080", .path = "/file.tar.gz", @@ -143,8 +120,6 @@ namespace nix { auto parsed = parseURL(s); ParsedURL expected { - .url = "http://[fe80::818c:da4d:8975:415c\%enp0s25]:8080", - .base = "http://[fe80::818c:da4d:8975:415c\%enp0s25]:8080", .scheme = "http", .authority = "[fe80::818c:da4d:8975:415c\%enp0s25]:8080", .path = "", @@ -161,8 +136,6 @@ namespace nix { auto parsed = parseURL(s); ParsedURL expected { - .url = "http://[2a02:8071:8192:c100:311d:192d:81ac:11ea]:8080", - .base = "http://[2a02:8071:8192:c100:311d:192d:81ac:11ea]:8080", .scheme = "http", .authority = "[2a02:8071:8192:c100:311d:192d:81ac:11ea]:8080", .path = "", @@ -185,8 +158,6 @@ namespace nix { auto parsed = parseURL(s); ParsedURL expected { - .url = "http://user:pass@www.example.org/file.tar.gz", - .base = "http://user:pass@www.example.org/file.tar.gz", .scheme = "http", .authority = "user:pass@www.example.org:8080", .path = "/file.tar.gz", @@ -203,8 +174,6 @@ namespace nix { auto parsed = parseURL(s); ParsedURL expected { - .url = "", - .base = "", .scheme = "file", .authority = "", .path = "/none/of//your/business", @@ -228,8 +197,6 @@ namespace nix { auto parsed = parseURL(s); ParsedURL expected { - .url = "ftp://ftp.nixos.org/downloads/nixos.iso", - .base = "ftp://ftp.nixos.org/downloads/nixos.iso", .scheme = "ftp", .authority = "ftp.nixos.org", .path = "/downloads/nixos.iso", diff --git a/src/libutil-tests/util.cc b/src/libutil-tests/util.cc index a3f7c720a5c..534731c6c08 100644 --- a/src/libutil-tests/util.cc +++ b/src/libutil-tests/util.cc @@ -1,8 +1,8 @@ -#include "util.hh" -#include "types.hh" -#include "file-system.hh" -#include "terminal.hh" -#include "strings.hh" +#include "nix/util/util.hh" +#include "nix/util/types.hh" +#include "nix/util/file-system.hh" +#include "nix/util/terminal.hh" +#include "nix/util/strings.hh" #include #include @@ -79,7 +79,7 @@ TEST(base64Encode, encodeAndDecodeNonPrintable) auto encoded = base64Encode(s); auto decoded = base64Decode(encoded); - EXPECT_EQ(decoded.length(), 255); + EXPECT_EQ(decoded.length(), 255u); ASSERT_EQ(decoded, s); } diff --git a/src/libutil-tests/xml-writer.cc b/src/libutil-tests/xml-writer.cc index adcde25c9f1..000af700c3a 100644 --- a/src/libutil-tests/xml-writer.cc +++ b/src/libutil-tests/xml-writer.cc @@ -1,4 +1,4 @@ -#include "xml-writer.hh" +#include "nix/util/xml-writer.hh" #include #include diff --git a/src/libutil/archive.cc b/src/libutil/archive.cc index 20d8a1e09be..9069e4b495f 100644 --- a/src/libutil/archive.cc +++ b/src/libutil/archive.cc @@ -5,19 +5,19 @@ #include // for strcasecmp -#include "archive.hh" -#include "config-global.hh" -#include "posix-source-accessor.hh" -#include "source-path.hh" -#include "file-system.hh" -#include "signals.hh" +#include "nix/util/archive.hh" +#include "nix/util/config-global.hh" +#include "nix/util/posix-source-accessor.hh" +#include "nix/util/source-path.hh" +#include "nix/util/file-system.hh" +#include "nix/util/signals.hh" namespace nix { struct ArchiveSettings : Config { Setting useCaseHack{this, - #if __APPLE__ + #ifdef __APPLE__ true, #else false, @@ -72,7 +72,7 @@ void SourceAccessor::dumpPath( /* If we're on a case-insensitive system like macOS, undo the case hack applied by restorePath(). */ - std::map unhacked; + StringMap unhacked; for (auto & i : readDirectory(path)) if (archiveSettings.useCaseHack) { std::string name(i.first); diff --git a/src/libutil/args.cc b/src/libutil/args.cc index 05ecf724ef6..d8d004e6fc3 100644 --- a/src/libutil/args.cc +++ b/src/libutil/args.cc @@ -1,10 +1,10 @@ -#include "args.hh" -#include "args/root.hh" -#include "hash.hh" -#include "environment-variables.hh" -#include "signals.hh" -#include "users.hh" -#include "json-utils.hh" +#include "nix/util/args.hh" +#include "nix/util/args/root.hh" +#include "nix/util/hash.hh" +#include "nix/util/environment-variables.hh" +#include "nix/util/signals.hh" +#include "nix/util/users.hh" +#include "nix/util/json-utils.hh" #include #include @@ -593,7 +593,7 @@ MultiCommand::MultiCommand(std::string_view commandName, const Commands & comman assert(!command); auto i = commands.find(s); if (i == commands.end()) { - std::set commandNames; + StringSet commandNames; for (auto & [name, _] : commands) commandNames.insert(name); auto suggestions = Suggestions::bestMatches(commandNames, s); @@ -647,4 +647,25 @@ nlohmann::json MultiCommand::toJSON() return res; } +Strings::iterator MultiCommand::rewriteArgs(Strings & args, Strings::iterator pos) +{ + if (command) + return command->second->rewriteArgs(args, pos); + + if (aliasUsed || pos == args.end()) return pos; + auto arg = *pos; + auto i = aliases.find(arg); + if (i == aliases.end()) return pos; + auto & info = i->second; + if (info.status == AliasStatus::Deprecated) { + warn("'%s' is a deprecated alias for '%s'", + arg, concatStringsSep(" ", info.replacement)); + } + pos = args.erase(pos); + for (auto j = info.replacement.rbegin(); j != info.replacement.rend(); ++j) + pos = args.insert(pos, *j); + aliasUsed = true; + return pos; +} + } diff --git a/src/libutil/build-utils-meson b/src/libutil/build-utils-meson deleted file mode 120000 index 5fff21bab55..00000000000 --- a/src/libutil/build-utils-meson +++ /dev/null @@ -1 +0,0 @@ -../../build-utils-meson \ No newline at end of file diff --git a/src/libutil/canon-path.cc b/src/libutil/canon-path.cc index 03db6378a82..33ac700f013 100644 --- a/src/libutil/canon-path.cc +++ b/src/libutil/canon-path.cc @@ -1,7 +1,7 @@ -#include "canon-path.hh" -#include "util.hh" -#include "file-path-impl.hh" -#include "strings-inline.hh" +#include "nix/util/canon-path.hh" +#include "nix/util/util.hh" +#include "nix/util/file-path-impl.hh" +#include "nix/util/strings-inline.hh" namespace nix { diff --git a/src/libutil/compression.cc b/src/libutil/compression.cc index d2702856591..0e38620d413 100644 --- a/src/libutil/compression.cc +++ b/src/libutil/compression.cc @@ -1,8 +1,8 @@ -#include "compression.hh" -#include "signals.hh" -#include "tarfile.hh" -#include "finally.hh" -#include "logging.hh" +#include "nix/util/compression.hh" +#include "nix/util/signals.hh" +#include "nix/util/tarfile.hh" +#include "nix/util/finally.hh" +#include "nix/util/logging.hh" #include #include diff --git a/src/libutil/compute-levels.cc b/src/libutil/compute-levels.cc index 19eaedfa8d1..dd221bd70f7 100644 --- a/src/libutil/compute-levels.cc +++ b/src/libutil/compute-levels.cc @@ -1,7 +1,10 @@ -#include "types.hh" +#include "nix/util/types.hh" + +#include "util-config-private.hh" #if HAVE_LIBCPUID -#include +# include +# include #endif namespace nix { @@ -10,61 +13,21 @@ namespace nix { StringSet computeLevels() { StringSet levels; + struct cpu_id_t data; - if (!cpuid_present()) - return levels; - - cpu_raw_data_t raw; - cpu_id_t data; - - if (cpuid_get_raw_data(&raw) < 0) - return levels; - - if (cpu_identify(&raw, &data) < 0) - return levels; - - if (!(data.flags[CPU_FEATURE_CMOV] && - data.flags[CPU_FEATURE_CX8] && - data.flags[CPU_FEATURE_FPU] && - data.flags[CPU_FEATURE_FXSR] && - data.flags[CPU_FEATURE_MMX] && - data.flags[CPU_FEATURE_SSE] && - data.flags[CPU_FEATURE_SSE2])) - return levels; - - levels.insert("x86_64-v1"); - - if (!(data.flags[CPU_FEATURE_CX16] && - data.flags[CPU_FEATURE_LAHF_LM] && - data.flags[CPU_FEATURE_POPCNT] && - // SSE3 - data.flags[CPU_FEATURE_PNI] && - data.flags[CPU_FEATURE_SSSE3] && - data.flags[CPU_FEATURE_SSE4_1] && - data.flags[CPU_FEATURE_SSE4_2])) - return levels; - - levels.insert("x86_64-v2"); - - if (!(data.flags[CPU_FEATURE_AVX] && - data.flags[CPU_FEATURE_AVX2] && - data.flags[CPU_FEATURE_F16C] && - data.flags[CPU_FEATURE_FMA3] && - // LZCNT - data.flags[CPU_FEATURE_ABM] && - data.flags[CPU_FEATURE_MOVBE])) - return levels; - - levels.insert("x86_64-v3"); + const std::map feature_strings = { + { FEATURE_LEVEL_X86_64_V1, "x86_64-v1" }, + { FEATURE_LEVEL_X86_64_V2, "x86_64-v2" }, + { FEATURE_LEVEL_X86_64_V3, "x86_64-v3" }, + { FEATURE_LEVEL_X86_64_V4, "x86_64-v4" }, + }; - if (!(data.flags[CPU_FEATURE_AVX512F] && - data.flags[CPU_FEATURE_AVX512BW] && - data.flags[CPU_FEATURE_AVX512CD] && - data.flags[CPU_FEATURE_AVX512DQ] && - data.flags[CPU_FEATURE_AVX512VL])) + if (cpu_identify(NULL, &data) < 0) return levels; - levels.insert("x86_64-v4"); + for (auto & [level, levelString] : feature_strings) + if (data.feature_level >= level) + levels.insert(levelString); return levels; } diff --git a/src/libutil/config-global.cc b/src/libutil/config-global.cc index 3ed1dd1d31c..94d71544333 100644 --- a/src/libutil/config-global.cc +++ b/src/libutil/config-global.cc @@ -1,4 +1,4 @@ -#include "config-global.hh" +#include "nix/util/config-global.hh" #include @@ -6,7 +6,7 @@ namespace nix { bool GlobalConfig::set(const std::string & name, const std::string & value) { - for (auto & config : *configRegistrations) + for (auto & config : configRegistrations()) if (config->set(name, value)) return true; @@ -17,20 +17,20 @@ bool GlobalConfig::set(const std::string & name, const std::string & value) void GlobalConfig::getSettings(std::map & res, bool overriddenOnly) { - for (auto & config : *configRegistrations) + for (auto & config : configRegistrations()) config->getSettings(res, overriddenOnly); } void GlobalConfig::resetOverridden() { - for (auto & config : *configRegistrations) + for (auto & config : configRegistrations()) config->resetOverridden(); } nlohmann::json GlobalConfig::toJSON() { auto res = nlohmann::json::object(); - for (const auto & config : *configRegistrations) + for (const auto & config : configRegistrations()) res.update(config->toJSON()); return res; } @@ -47,19 +47,15 @@ std::string GlobalConfig::toKeyValue() void GlobalConfig::convertToArgs(Args & args, const std::string & category) { - for (auto & config : *configRegistrations) + for (auto & config : configRegistrations()) config->convertToArgs(args, category); } GlobalConfig globalConfig; -GlobalConfig::ConfigRegistrations * GlobalConfig::configRegistrations; - GlobalConfig::Register::Register(Config * config) { - if (!configRegistrations) - configRegistrations = new ConfigRegistrations; - configRegistrations->emplace_back(config); + configRegistrations().emplace_back(config); } ExperimentalFeatureSettings experimentalFeatureSettings; diff --git a/src/libutil/config.cc b/src/libutil/configuration.cc similarity index 97% rename from src/libutil/config.cc rename to src/libutil/configuration.cc index ca8480304d2..314ae34db4b 100644 --- a/src/libutil/config.cc +++ b/src/libutil/configuration.cc @@ -1,16 +1,16 @@ -#include "config.hh" -#include "args.hh" -#include "abstract-setting-to-json.hh" -#include "environment-variables.hh" -#include "experimental-features.hh" -#include "util.hh" -#include "file-system.hh" +#include "nix/util/configuration.hh" +#include "nix/util/args.hh" +#include "nix/util/abstract-setting-to-json.hh" +#include "nix/util/environment-variables.hh" +#include "nix/util/experimental-features.hh" +#include "nix/util/util.hh" +#include "nix/util/file-system.hh" -#include "config-impl.hh" +#include "nix/util/config-impl.hh" #include -#include "strings.hh" +#include "nix/util/strings.hh" namespace nix { @@ -220,7 +220,7 @@ void Config::convertToArgs(Args & args, const std::string & category) AbstractSetting::AbstractSetting( const std::string & name, const std::string & description, - const std::set & aliases, + const StringSet & aliases, std::optional experimentalFeature) : name(name) , description(stripIndentation(description)) @@ -428,7 +428,7 @@ PathSetting::PathSetting(Config * options, const Path & def, const std::string & name, const std::string & description, - const std::set & aliases) + const StringSet & aliases) : BaseSetting(def, true, name, description, aliases) { options->addSetting(this); @@ -444,7 +444,7 @@ OptionalPathSetting::OptionalPathSetting(Config * options, const std::optional & def, const std::string & name, const std::string & description, - const std::set & aliases) + const StringSet & aliases) : BaseSetting>(def, true, name, description, aliases) { options->addSetting(this); diff --git a/src/libutil/current-process.cc b/src/libutil/current-process.cc index ac01f441e6b..1afefbcb25b 100644 --- a/src/libutil/current-process.cc +++ b/src/libutil/current-process.cc @@ -1,33 +1,34 @@ #include #include -#include "current-process.hh" -#include "util.hh" -#include "finally.hh" -#include "file-system.hh" -#include "processes.hh" -#include "signals.hh" +#include "nix/util/current-process.hh" +#include "nix/util/util.hh" +#include "nix/util/finally.hh" +#include "nix/util/file-system.hh" +#include "nix/util/processes.hh" +#include "nix/util/signals.hh" #include #ifdef __APPLE__ # include #endif -#if __linux__ +#ifdef __linux__ # include -# include "cgroup.hh" -# include "namespaces.hh" +# include "nix/util/cgroup.hh" +# include "nix/util/linux-namespaces.hh" #endif -#ifndef _WIN32 -# include +#ifdef __FreeBSD__ +# include +# include #endif namespace nix { unsigned int getMaxCPU() { - #if __linux__ + #ifdef __linux__ try { auto cgroupFS = getCgroupFS(); if (!cgroupFS) return 0; @@ -55,13 +56,13 @@ unsigned int getMaxCPU() ////////////////////////////////////////////////////////////////////// +#ifndef _WIN32 size_t savedStackSize = 0; void setStackSize(size_t stackSize) { - #ifndef _WIN32 struct rlimit limit; - if (getrlimit(RLIMIT_STACK, &limit) == 0 && limit.rlim_cur < stackSize) { + if (getrlimit(RLIMIT_STACK, &limit) == 0 && static_cast(limit.rlim_cur) < stackSize) { savedStackSize = limit.rlim_cur; limit.rlim_cur = std::min(static_cast(stackSize), limit.rlim_max); if (setrlimit(RLIMIT_STACK, &limit) != 0) { @@ -77,31 +78,8 @@ void setStackSize(size_t stackSize) ); } } - #else - ULONG_PTR stackLow, stackHigh; - GetCurrentThreadStackLimits(&stackLow, &stackHigh); - ULONG maxStackSize = stackHigh - stackLow; - ULONG currStackSize = 0; - // This retrieves the current promised stack size - SetThreadStackGuarantee(&currStackSize); - if (currStackSize < stackSize) { - savedStackSize = currStackSize; - ULONG newStackSize = std::min(static_cast(stackSize), maxStackSize); - if (SetThreadStackGuarantee(&newStackSize) == 0) { - logger->log( - lvlError, - HintFmt( - "Failed to increase stack size from %1% to %2% (maximum allowed stack size: %3%): %4%", - savedStackSize, - stackSize, - maxStackSize, - std::to_string(GetLastError()) - ).str() - ); - } - } - #endif } +#endif void restoreProcessContext(bool restoreMounts) { @@ -109,7 +87,7 @@ void restoreProcessContext(bool restoreMounts) unix::restoreSignals(); #endif if (restoreMounts) { - #if __linux__ + #ifdef __linux__ restoreMountNamespace(); #endif } @@ -133,15 +111,33 @@ std::optional getSelfExe() { static auto cached = []() -> std::optional { - #if __linux__ || __GNU__ + #if defined(__linux__) || defined(__GNU__) return readLink("/proc/self/exe"); - #elif __APPLE__ + #elif defined(__APPLE__) char buf[1024]; uint32_t size = sizeof(buf); if (_NSGetExecutablePath(buf, &size) == 0) return buf; else return std::nullopt; + #elif defined(__FreeBSD__) + int sysctlName[] = { + CTL_KERN, + KERN_PROC, + KERN_PROC_PATHNAME, + -1, + }; + size_t pathLen = 0; + if (sysctl(sysctlName, sizeof(sysctlName) / sizeof(sysctlName[0]), nullptr, &pathLen, nullptr, 0) < 0) { + return std::nullopt; + } + + std::vector path(pathLen); + if (sysctl(sysctlName, sizeof(sysctlName) / sizeof(sysctlName[0]), path.data(), &pathLen, nullptr, 0) < 0) { + return std::nullopt; + } + + return Path(path.begin(), path.end()); #else return std::nullopt; #endif diff --git a/src/libutil/english.cc b/src/libutil/english.cc index 8c93c915662..e697b8c3051 100644 --- a/src/libutil/english.cc +++ b/src/libutil/english.cc @@ -1,4 +1,4 @@ -#include "english.hh" +#include "nix/util/english.hh" namespace nix { diff --git a/src/libutil/environment-variables.cc b/src/libutil/environment-variables.cc index 5947cf742ac..adae177347c 100644 --- a/src/libutil/environment-variables.cc +++ b/src/libutil/environment-variables.cc @@ -1,5 +1,5 @@ -#include "util.hh" -#include "environment-variables.hh" +#include "nix/util/util.hh" +#include "nix/util/environment-variables.hh" extern char ** environ __attribute__((weak)); @@ -21,9 +21,9 @@ std::optional getEnvNonEmpty(const std::string & key) return value; } -std::map getEnv() +StringMap getEnv() { - std::map env; + StringMap env; for (size_t i = 0; environ[i]; ++i) { auto s = environ[i]; auto eq = strchr(s, '='); @@ -41,7 +41,7 @@ void clearEnv() unsetenv(name.first.c_str()); } -void replaceEnv(const std::map & newEnv) +void replaceEnv(const StringMap & newEnv) { clearEnv(); for (auto & newEnvVar : newEnv) diff --git a/src/libutil/error.cc b/src/libutil/error.cc index ccd008c7c33..049555ea3fc 100644 --- a/src/libutil/error.cc +++ b/src/libutil/error.cc @@ -1,19 +1,19 @@ #include -#include "error.hh" -#include "environment-variables.hh" -#include "signals.hh" -#include "terminal.hh" -#include "position.hh" +#include "nix/util/error.hh" +#include "nix/util/environment-variables.hh" +#include "nix/util/signals.hh" +#include "nix/util/terminal.hh" +#include "nix/util/position.hh" #include #include -#include "serialise.hh" +#include "nix/util/serialise.hh" #include namespace nix { -void BaseError::addTrace(std::shared_ptr && e, HintFmt hint, TracePrint print) +void BaseError::addTrace(std::shared_ptr && e, HintFmt hint, TracePrint print) { err.traces.push_front(Trace { .pos = std::move(e), .hint = hint, .print = print }); } @@ -146,7 +146,7 @@ static bool printUnknownLocations = getEnv("_NIX_EVAL_SHOW_UNKNOWN_LOCATIONS").h * * @return true if a position was printed. */ -static bool printPosMaybe(std::ostream & oss, std::string_view indent, const std::shared_ptr & pos) { +static bool printPosMaybe(std::ostream & oss, std::string_view indent, const std::shared_ptr & pos) { bool hasPos = pos && *pos; if (hasPos) { oss << indent << ANSI_BLUE << "at " ANSI_WARNING << *pos << ANSI_NORMAL << ":"; diff --git a/src/libutil/executable-path.cc b/src/libutil/executable-path.cc index ebd522a415c..75ab91f3a16 100644 --- a/src/libutil/executable-path.cc +++ b/src/libutil/executable-path.cc @@ -1,15 +1,11 @@ -#include "environment-variables.hh" -#include "executable-path.hh" -#include "strings-inline.hh" -#include "util.hh" -#include "file-path-impl.hh" +#include "nix/util/environment-variables.hh" +#include "nix/util/executable-path.hh" +#include "nix/util/strings-inline.hh" +#include "nix/util/util.hh" +#include "nix/util/file-path-impl.hh" namespace nix { -namespace fs { -using namespace std::filesystem; -} - constexpr static const OsStringView path_var_separator{ &ExecutablePath::separator, 1, @@ -28,7 +24,7 @@ ExecutablePath ExecutablePath::parse(const OsString & path) auto strings = path.empty() ? (std::list{}) : basicSplitString, OsChar>(path, path_var_separator); - std::vector ret; + std::vector ret; ret.reserve(strings.size()); std::transform( @@ -36,7 +32,7 @@ ExecutablePath ExecutablePath::parse(const OsString & path) std::make_move_iterator(strings.end()), std::back_inserter(ret), [](OsString && str) { - return fs::path{ + return std::filesystem::path{ str.empty() // "A zero-length prefix is a legacy feature that // indicates the current working directory. It @@ -62,24 +58,25 @@ OsString ExecutablePath::render() const return basicConcatStringsSep(path_var_separator, path2); } -std::optional -ExecutablePath::findName(const OsString & exe, std::function isExecutable) const +std::optional +ExecutablePath::findName(const OsString & exe, std::function isExecutable) const { // "If the pathname being sought contains a , the search // through the path prefixes shall not be performed." // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html#tag_08_03 - assert(OsPathTrait::rfindPathSep(exe) == exe.npos); + assert(OsPathTrait::rfindPathSep(exe) == exe.npos); for (auto & dir : directories) { auto candidate = dir / exe; if (isExecutable(candidate)) - return std::filesystem::canonical(candidate); + return candidate.lexically_normal(); } return std::nullopt; } -fs::path ExecutablePath::findPath(const fs::path & exe, std::function isExecutable) const +std::filesystem::path ExecutablePath::findPath( + const std::filesystem::path & exe, std::function isExecutable) const { // "If the pathname being sought contains a , the search // through the path prefixes shall not be performed." diff --git a/src/libutil/exit.cc b/src/libutil/exit.cc index 73cd8b04ee8..3c59e46af20 100644 --- a/src/libutil/exit.cc +++ b/src/libutil/exit.cc @@ -1,4 +1,4 @@ -#include "exit.hh" +#include "nix/util/exit.hh" namespace nix { diff --git a/src/libutil/experimental-features.cc b/src/libutil/experimental-features.cc index a0c955816e9..88f3783f552 100644 --- a/src/libutil/experimental-features.cc +++ b/src/libutil/experimental-features.cc @@ -1,8 +1,8 @@ -#include "experimental-features.hh" -#include "fmt.hh" -#include "util.hh" +#include "nix/util/experimental-features.hh" +#include "nix/util/fmt.hh" +#include "nix/util/util.hh" -#include "nlohmann/json.hpp" +#include namespace nix { @@ -24,7 +24,7 @@ struct ExperimentalFeatureDetails * feature, we either have no issue at all if few features are not added * at the end of the list, or a proper merge conflict if they are. */ -constexpr size_t numXpFeatures = 1 + static_cast(Xp::PipeOperators); +constexpr size_t numXpFeatures = 1 + static_cast(Xp::BLAKE3Hashes); constexpr std::array xpFeatureDetails = {{ { @@ -107,7 +107,7 @@ constexpr std::array xpFeatureDetails .name = "git-hashing", .description = R"( Allow creating (content-addressed) store objects which are hashed via Git's hashing algorithm. - These store objects will not be understandable by older versions of Nix. + These store objects aren't understandable by older versions of Nix. )", .trackingUrl = "https://github.com/NixOS/nix/milestone/41", }, @@ -125,6 +125,8 @@ constexpr std::array xpFeatureDetails runCommand "foo" { + # Optional: let Nix know "foo" requires the experimental feature + requiredSystemFeatures = [ "recursive-nix" ]; buildInputs = [ nix jq ]; NIX_PATH = "nixpkgs=${}"; } @@ -302,6 +304,14 @@ constexpr std::array xpFeatureDetails )", .trackingUrl = "https://github.com/NixOS/nix/milestone/55", }, + { + .tag = Xp::BLAKE3Hashes, + .name = "blake3-hashes", + .description = R"( + Enables support for BLAKE3 hashes. + )", + .trackingUrl = "", + }, }}; static_assert( @@ -348,7 +358,7 @@ nlohmann::json documentExperimentalFeatures() return (nlohmann::json) res; } -std::set parseFeatures(const std::set & rawFeatures) +std::set parseFeatures(const StringSet & rawFeatures) { std::set res; for (auto & rawFeature : rawFeatures) diff --git a/src/libutil/file-content-address.cc b/src/libutil/file-content-address.cc index 69301d9c8f4..d957816918d 100644 --- a/src/libutil/file-content-address.cc +++ b/src/libutil/file-content-address.cc @@ -1,7 +1,7 @@ -#include "file-content-address.hh" -#include "archive.hh" -#include "git.hh" -#include "source-path.hh" +#include "nix/util/file-content-address.hh" +#include "nix/util/archive.hh" +#include "nix/util/git.hh" +#include "nix/util/source-path.hh" namespace nix { @@ -22,7 +22,7 @@ FileSerialisationMethod parseFileSerialisationMethod(std::string_view input) if (ret) return *ret; else - throw UsageError("Unknown file serialiation method '%s', expect `flat` or `nar`"); + throw UsageError("Unknown file serialiation method '%s', expect `flat` or `nar`", input); } @@ -35,7 +35,7 @@ FileIngestionMethod parseFileIngestionMethod(std::string_view input) if (ret) return static_cast(*ret); else - throw UsageError("Unknown file ingestion method '%s', expect `flat`, `nar`, or `git`"); + throw UsageError("Unknown file ingestion method '%s', expect `flat`, `nar`, or `git`", input); } } @@ -93,7 +93,7 @@ void restorePath( { switch (method) { case FileSerialisationMethod::Flat: - writeFile(path, source, 0666, startFsync); + writeFile(path, source, 0666, startFsync ? FsSync::Yes : FsSync::No); break; case FileSerialisationMethod::NixArchive: restorePath(path, source, startFsync); diff --git a/src/libutil/file-descriptor.cc b/src/libutil/file-descriptor.cc index 542c33f3ba8..9e0827442a1 100644 --- a/src/libutil/file-descriptor.cc +++ b/src/libutil/file-descriptor.cc @@ -1,15 +1,12 @@ -#include "file-system.hh" -#include "signals.hh" -#include "finally.hh" -#include "serialise.hh" -#include "util.hh" +#include "nix/util/serialise.hh" +#include "nix/util/util.hh" #include #include #ifdef _WIN32 # include # include -# include "windows-error.hh" +# include "nix/util/windows-error.hh" #endif namespace nix { @@ -101,7 +98,7 @@ void AutoCloseFD::fsync() const result = #ifdef _WIN32 ::FlushFileBuffers(fd) -#elif __APPLE__ +#elif defined(__APPLE__) ::fcntl(fd, F_FULLFSYNC) #else ::fsync(fd) @@ -116,7 +113,7 @@ void AutoCloseFD::fsync() const void AutoCloseFD::startFsync() const { -#if __linux__ +#ifdef __linux__ if (fd != -1) { /* Ignore failure, since fsync must be run later anyway. This is just a performance optimization. */ ::sync_file_range(fd, 0, 0, SYNC_FILE_RANGE_WRITE); diff --git a/src/libutil/file-system.cc b/src/libutil/file-system.cc index 829700336c1..79e6cf3546c 100644 --- a/src/libutil/file-system.cc +++ b/src/libutil/file-system.cc @@ -1,19 +1,19 @@ -#include "environment-variables.hh" -#include "file-system.hh" -#include "file-path.hh" -#include "file-path-impl.hh" -#include "signals.hh" -#include "finally.hh" -#include "serialise.hh" -#include "util.hh" +#include "nix/util/environment-variables.hh" +#include "nix/util/file-system.hh" +#include "nix/util/file-path.hh" +#include "nix/util/file-path-impl.hh" +#include "nix/util/signals.hh" +#include "nix/util/finally.hh" +#include "nix/util/serialise.hh" +#include "nix/util/util.hh" #include +#include #include #include #include #include #include -#include #include #include @@ -21,24 +21,50 @@ #include #include +#include + +#ifdef __FreeBSD__ +# include +# include +#endif + #ifdef _WIN32 # include #endif -#include "strings-inline.hh" - namespace nix { -namespace fs { using namespace std::filesystem; } +DirectoryIterator::DirectoryIterator(const std::filesystem::path& p) { + try { + // **Attempt to create the underlying directory_iterator** + it_ = std::filesystem::directory_iterator(p); + } catch (const std::filesystem::filesystem_error& e) { + // **Catch filesystem_error and throw SysError** + // Adapt the error message as needed for SysError + throw SysError("cannot read directory %s", p); + } +} + +DirectoryIterator& DirectoryIterator::operator++() { + // **Attempt to increment the underlying iterator** + std::error_code ec; + it_.increment(ec); + if (ec) { + // Try to get path info if possible, might fail if iterator is bad + try { + if (it_ != std::filesystem::directory_iterator{}) { + throw SysError("cannot read directory past %s: %s", it_->path(), ec.message()); + } + } catch (...) { + throw SysError("cannot read directory"); + } + } + return *this; +} -/** - * Treat the string as possibly an absolute path, by inspecting the - * start of it. Return whether it was probably intended to be - * absolute. - */ -static bool isAbsolute(PathView path) +bool isAbsolute(PathView path) { - return fs::path { path }.is_absolute(); + return std::filesystem::path { path }.is_absolute(); } @@ -87,7 +113,7 @@ Path canonPath(PathView path, bool resolveSymlinks) throw Error("not an absolute path: '%1%'", path); // For Windows - auto rootName = fs::path { path }.root_name(); + auto rootName = std::filesystem::path { path }.root_name(); /* This just exists because we cannot set the target of `remaining` (the callback parameter) directly to a newly-constructed string, @@ -102,9 +128,9 @@ Path canonPath(PathView path, bool resolveSymlinks) path, [&followCount, &temp, maxFollow, resolveSymlinks] (std::string & result, std::string_view & remaining) { - if (resolveSymlinks && fs::is_symlink(result)) { + if (resolveSymlinks && std::filesystem::is_symlink(result)) { if (++followCount >= maxFollow) - throw Error("infinite symlink recursion in path '%0%'", remaining); + throw Error("infinite symlink recursion in path '%1%'", remaining); remaining = (temp = concatStrings(readLink(result), remaining)); if (isAbsolute(remaining)) { /* restart for symlinks pointing to absolute path */ @@ -131,7 +157,7 @@ Path dirOf(const PathView path) Path::size_type pos = OsPathTrait::rfindPathSep(path); if (pos == path.npos) return "."; - return fs::path{path}.parent_path().string(); + return std::filesystem::path{path}.parent_path().string(); } @@ -154,16 +180,18 @@ std::string_view baseNameOf(std::string_view path) } -bool isInDir(std::string_view path, std::string_view dir) +bool isInDir(const std::filesystem::path & path, const std::filesystem::path & dir) { - return path.substr(0, 1) == "/" - && path.substr(0, dir.size()) == dir - && path.size() >= dir.size() + 2 - && path[dir.size()] == '/'; + /* Note that while the standard doesn't guarantee this, the + `lexically_*` functions should do no IO and not throw. */ + auto rel = path.lexically_relative(dir); + /* Method from + https://stackoverflow.com/questions/62503197/check-if-path-contains-another-in-c++ */ + return !rel.empty() && rel.native()[0] != OS_STR('.'); } -bool isDirOrInDir(std::string_view path, std::string_view dir) +bool isDirOrInDir(const std::filesystem::path & path, const std::filesystem::path & dir) { return path == dir || isInDir(path, dir); } @@ -206,9 +234,9 @@ std::optional maybeLstat(const Path & path) } -bool pathExists(const Path & path) +bool pathExists(const std::filesystem::path & path) { - return maybeLstat(path).has_value(); + return maybeLstat(path.string()).has_value(); } bool pathAccessible(const std::filesystem::path & path) @@ -226,7 +254,7 @@ bool pathAccessible(const std::filesystem::path & path) Path readLink(const Path & path) { checkInterrupt(); - return fs::read_symlink(path).string(); + return std::filesystem::read_symlink(path).string(); } @@ -248,9 +276,22 @@ std::string readFile(const std::filesystem::path & path) return readFile(os_string_to_string(PathViewNG { path })); } - -void readFile(const Path & path, Sink & sink) +void readFile(const Path & path, Sink & sink, bool memory_map) { + // Memory-map the file for faster processing where possible. + if (memory_map) { + try { + boost::iostreams::mapped_file_source mmap(path); + if (mmap.is_open()) { + sink({mmap.data(), mmap.size()}); + return; + } + } catch (const boost::exception & e) { + } + debug("memory-mapping failed for path: %s", path); + } + + // Stream the file instead if memory-mapping fails or is disabled. AutoCloseFD fd = toDescriptor(open(path.c_str(), O_RDONLY // TODO #ifndef _WIN32 @@ -263,7 +304,7 @@ void readFile(const Path & path, Sink & sink) } -void writeFile(const Path & path, std::string_view s, mode_t mode, bool sync) +void writeFile(const Path & path, std::string_view s, mode_t mode, FsSync sync) { AutoCloseFD fd = toDescriptor(open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT // TODO @@ -273,22 +314,29 @@ void writeFile(const Path & path, std::string_view s, mode_t mode, bool sync) , mode)); if (!fd) throw SysError("opening file '%1%'", path); + + writeFile(fd, path, s, mode, sync); + + /* Close explicitly to propagate the exceptions. */ + fd.close(); +} + +void writeFile(AutoCloseFD & fd, const Path & origPath, std::string_view s, mode_t mode, FsSync sync) +{ + assert(fd); try { writeFull(fd.get(), s); + + if (sync == FsSync::Yes) + fd.fsync(); + } catch (Error & e) { - e.addTrace({}, "writing file '%1%'", path); + e.addTrace({}, "writing file '%1%'", origPath); throw; } - if (sync) - fd.fsync(); - // Explicitly close to make sure exceptions are propagated. - fd.close(); - if (sync) - syncParent(path); } - -void writeFile(const Path & path, Source & source, mode_t mode, bool sync) +void writeFile(const Path & path, Source & source, mode_t mode, FsSync sync) { AutoCloseFD fd = toDescriptor(open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT // TODO @@ -312,11 +360,11 @@ void writeFile(const Path & path, Source & source, mode_t mode, bool sync) e.addTrace({}, "writing file '%1%'", path); throw; } - if (sync) + if (sync == FsSync::Yes) fd.fsync(); // Explicitly close to make sure exceptions are propagated. fd.close(); - if (sync) + if (sync == FsSync::Yes) syncParent(path); } @@ -328,10 +376,17 @@ void syncParent(const Path & path) fd.fsync(); } +#ifdef __FreeBSD__ +#define MOUNTEDPATHS_PARAM , std::set &mountedPaths +#define MOUNTEDPATHS_ARG , mountedPaths +#else +#define MOUNTEDPATHS_PARAM +#define MOUNTEDPATHS_ARG +#endif void recursiveSync(const Path & path) { - /* If it's a file, just fsync and return. */ + /* If it's a file or symlink, just fsync and return. */ auto st = lstat(path); if (S_ISREG(st.st_mode)) { AutoCloseFD fd = toDescriptor(open(path.c_str(), O_RDONLY, 0)); @@ -339,21 +394,22 @@ void recursiveSync(const Path & path) throw SysError("opening file '%1%'", path); fd.fsync(); return; - } + } else if (S_ISLNK(st.st_mode)) + return; /* Otherwise, perform a depth-first traversal of the directory and fsync all the files. */ - std::deque dirsToEnumerate; + std::deque dirsToEnumerate; dirsToEnumerate.push_back(path); - std::vector dirsToFsync; + std::vector dirsToFsync; while (!dirsToEnumerate.empty()) { auto currentDir = dirsToEnumerate.back(); dirsToEnumerate.pop_back(); - for (auto & entry : std::filesystem::directory_iterator(currentDir)) { + for (auto & entry : DirectoryIterator(currentDir)) { auto st = entry.symlink_status(); - if (fs::is_directory(st)) { + if (std::filesystem::is_directory(st)) { dirsToEnumerate.emplace_back(entry.path()); - } else if (fs::is_regular_file(st)) { + } else if (std::filesystem::is_regular_file(st)) { AutoCloseFD fd = toDescriptor(open(entry.path().string().c_str(), O_RDONLY, 0)); if (!fd) throw SysError("opening file '%1%'", entry.path()); @@ -373,18 +429,27 @@ void recursiveSync(const Path & path) } -static void _deletePath(Descriptor parentfd, const fs::path & path, uint64_t & bytesFreed) +static void _deletePath(Descriptor parentfd, const std::filesystem::path & path, uint64_t & bytesFreed, std::exception_ptr & ex MOUNTEDPATHS_PARAM) { #ifndef _WIN32 checkInterrupt(); - std::string name(baseNameOf(path.native())); +#ifdef __FreeBSD__ + // In case of emergency (unmount fails for some reason) not recurse into mountpoints. + // This prevents us from tearing up the nullfs-mounted nix store. + if (mountedPaths.find(path) != mountedPaths.end()) { + return; + } +#endif + + std::string name(path.filename()); + assert(name != "." && name != ".." && !name.empty()); struct stat st; if (fstatat(parentfd, name.c_str(), &st, AT_SYMLINK_NOFOLLOW) == -1) { if (errno == ENOENT) return; - throw SysError("getting status of '%1%'", path); + throw SysError("getting status of %1%", path); } if (!S_ISDIR(st.st_mode)) { @@ -416,30 +481,37 @@ static void _deletePath(Descriptor parentfd, const fs::path & path, uint64_t & b const auto PERM_MASK = S_IRUSR | S_IWUSR | S_IXUSR; if ((st.st_mode & PERM_MASK) != PERM_MASK) { if (fchmodat(parentfd, name.c_str(), st.st_mode | PERM_MASK, 0) == -1) - throw SysError("chmod '%1%'", path); + throw SysError("chmod %1%", path); } - int fd = openat(parentfd, path.c_str(), O_RDONLY); + int fd = openat(parentfd, name.c_str(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW); if (fd == -1) - throw SysError("opening directory '%1%'", path); + throw SysError("opening directory %1%", path); AutoCloseDir dir(fdopendir(fd)); if (!dir) - throw SysError("opening directory '%1%'", path); + throw SysError("opening directory %1%", path); struct dirent * dirent; while (errno = 0, dirent = readdir(dir.get())) { /* sic */ checkInterrupt(); std::string childName = dirent->d_name; if (childName == "." || childName == "..") continue; - _deletePath(dirfd(dir.get()), path + "/" + childName, bytesFreed); + _deletePath(dirfd(dir.get()), path / childName, bytesFreed, ex MOUNTEDPATHS_ARG); } - if (errno) throw SysError("reading directory '%1%'", path); + if (errno) throw SysError("reading directory %1%", path); } int flags = S_ISDIR(st.st_mode) ? AT_REMOVEDIR : 0; if (unlinkat(parentfd, name.c_str(), flags) == -1) { if (errno == ENOENT) return; - throw SysError("cannot unlink '%1%'", path); + try { + throw SysError("cannot unlink %1%", path); + } catch (...) { + if (!ex) + ex = std::current_exception(); + else + ignoreExceptionExceptInterrupt(); + } } #else // TODO implement @@ -447,23 +519,27 @@ static void _deletePath(Descriptor parentfd, const fs::path & path, uint64_t & b #endif } -static void _deletePath(const fs::path & path, uint64_t & bytesFreed) +static void _deletePath(const std::filesystem::path & path, uint64_t & bytesFreed MOUNTEDPATHS_PARAM) { - Path dir = dirOf(path.string()); - if (dir == "") - dir = "/"; + assert(path.is_absolute()); + assert(path.parent_path() != path); - AutoCloseFD dirfd = toDescriptor(open(dir.c_str(), O_RDONLY)); + AutoCloseFD dirfd = toDescriptor(open(path.parent_path().string().c_str(), O_RDONLY)); if (!dirfd) { if (errno == ENOENT) return; - throw SysError("opening directory '%1%'", path); + throw SysError("opening directory %s", path.parent_path()); } - _deletePath(dirfd.get(), path, bytesFreed); + std::exception_ptr ex; + + _deletePath(dirfd.get(), path, bytesFreed, ex MOUNTEDPATHS_ARG); + + if (ex) + std::rethrow_exception(ex); } -void deletePath(const fs::path & path) +void deletePath(const std::filesystem::path & path) { uint64_t dummy; deletePath(path, dummy); @@ -479,21 +555,33 @@ void createDir(const Path & path, mode_t mode) throw SysError("creating directory '%1%'", path); } -void createDirs(const Path & path) +void createDirs(const std::filesystem::path & path) { try { - fs::create_directories(path); - } catch (fs::filesystem_error & e) { - throw SysError("creating directory '%1%'", path); + std::filesystem::create_directories(path); + } catch (std::filesystem::filesystem_error & e) { + throw SysError("creating directory '%1%'", path.string()); } } -void deletePath(const fs::path & path, uint64_t & bytesFreed) +void deletePath(const std::filesystem::path & path, uint64_t & bytesFreed) { //Activity act(*logger, lvlDebug, "recursively deleting path '%1%'", path); +#ifdef __FreeBSD__ + std::set mountedPaths; + struct statfs *mntbuf; + int count; + if ((count = getmntinfo(&mntbuf, MNT_WAIT)) < 0) { + throw SysError("getmntinfo"); + } + + for (int i = 0; i < count; i++) { + mountedPaths.emplace(mntbuf[i].f_mntonname); + } +#endif bytesFreed = 0; - _deletePath(path, bytesFreed); + _deletePath(path, bytesFreed MOUNTEDPATHS_ARG); } @@ -514,7 +602,7 @@ AutoDelete::~AutoDelete() if (recursive) deletePath(_path); else { - fs::remove(_path); + std::filesystem::remove(_path); } } } catch (...) { @@ -527,7 +615,7 @@ void AutoDelete::cancel() del = false; } -void AutoDelete::reset(const fs::path & p, bool recursive) { +void AutoDelete::reset(const std::filesystem::path & p, bool recursive) { _path = p; this->recursive = recursive; del = true; @@ -535,38 +623,47 @@ void AutoDelete::reset(const fs::path & p, bool recursive) { ////////////////////////////////////////////////////////////////////// -////////////////////////////////////////////////////////////////////// +#ifdef __FreeBSD__ +AutoUnmount::AutoUnmount() : del{false} {} -std::string defaultTempDir() { - return getEnvNonEmpty("TMPDIR").value_or("/tmp"); -} +AutoUnmount::AutoUnmount(Path &p) : path(p), del(true) {} -static Path tempName(Path tmpRoot, const Path & prefix, bool includePid, - std::atomic & counter) +AutoUnmount::~AutoUnmount() { - tmpRoot = canonPath(tmpRoot.empty() ? defaultTempDir() : tmpRoot, true); - if (includePid) - return fmt("%1%/%2%-%3%-%4%", tmpRoot, prefix, getpid(), counter++); - else - return fmt("%1%/%2%-%3%", tmpRoot, prefix, counter++); + try { + if (del) { + if (unmount(path.c_str(), 0) < 0) { + throw SysError("Failed to unmount path %1%", path); + } + } + } catch (...) { + ignoreExceptionInDestructor(); + } } -Path createTempDir(const Path & tmpRoot, const Path & prefix, - bool includePid, bool useGlobalCounter, mode_t mode) +void AutoUnmount::cancel() { - static std::atomic globalCounter = 0; - std::atomic localCounter = 0; - auto & counter(useGlobalCounter ? globalCounter : localCounter); + del = false; +} +#endif + +////////////////////////////////////////////////////////////////////// + +std::string defaultTempDir() { + return getEnvNonEmpty("TMPDIR").value_or("/tmp"); +} +Path createTempDir(const Path & tmpRoot, const Path & prefix, mode_t mode) +{ while (1) { checkInterrupt(); - Path tmpDir = tempName(tmpRoot, prefix, includePid, counter); + Path tmpDir = makeTempPath(tmpRoot, prefix); if (mkdir(tmpDir.c_str() #ifndef _WIN32 // TODO abstract mkdir perms for Windows , mode #endif ) == 0) { -#if __FreeBSD__ +#ifdef __FreeBSD__ /* Explicitly set the group of the directory. This is to work around around problems caused by BSD's group ownership semantics (directories inherit the group of @@ -600,33 +697,41 @@ std::pair createTempFile(const Path & prefix) return {std::move(fd), tmpl}; } +Path makeTempPath(const Path & root, const Path & suffix) +{ + // start the counter at a random value to minimize issues with preexisting temp paths + static std::atomic counter(std::random_device{}()); + auto tmpRoot = canonPath(root.empty() ? defaultTempDir() : root, true); + return fmt("%1%/%2%-%3%-%4%", tmpRoot, suffix, getpid(), counter.fetch_add(1, std::memory_order_relaxed)); +} + void createSymlink(const Path & target, const Path & link) { try { - fs::create_symlink(target, link); - } catch (fs::filesystem_error & e) { + std::filesystem::create_symlink(target, link); + } catch (std::filesystem::filesystem_error & e) { throw SysError("creating symlink '%1%' -> '%2%'", link, target); } } -void replaceSymlink(const fs::path & target, const fs::path & link) +void replaceSymlink(const std::filesystem::path & target, const std::filesystem::path & link) { for (unsigned int n = 0; true; n++) { - auto tmp = link.parent_path() / fs::path{fmt(".%d_%s", n, link.filename().string())}; + auto tmp = link.parent_path() /std::filesystem::path{fmt(".%d_%s", n, link.filename().string())}; tmp = tmp.lexically_normal(); try { - fs::create_symlink(target, tmp); - } catch (fs::filesystem_error & e) { + std::filesystem::create_symlink(target, tmp); + } catch (std::filesystem::filesystem_error & e) { if (e.code() == std::errc::file_exists) continue; - throw SysError("creating symlink '%1%' -> '%2%'", tmp, target); + throw SysError("creating symlink %1% -> %2%", tmp, target); } try { - fs::rename(tmp, link); - } catch (fs::filesystem_error & e) { + std::filesystem::rename(tmp, link); + } catch (std::filesystem::filesystem_error & e) { if (e.code() == std::errc::file_exists) continue; - throw SysError("renaming '%1%' to '%2%'", tmp, link); + throw SysError("renaming %1% to %2%", tmp, link); } @@ -634,93 +739,36 @@ void replaceSymlink(const fs::path & target, const fs::path & link) } } -void setWriteTime( - const fs::path & path, - time_t accessedTime, - time_t modificationTime, - std::optional optIsSymlink) -{ -#ifdef _WIN32 - // FIXME use `fs::last_write_time`. - // - // Would be nice to use std::filesystem unconditionally, but - // doesn't support access time just modification time. - // - // System clock vs File clock issues also make that annoying. - warn("Changing file times is not yet implemented on Windows, path is '%s'", path); -#elif HAVE_UTIMENSAT && HAVE_DECL_AT_SYMLINK_NOFOLLOW - struct timespec times[2] = { - { - .tv_sec = accessedTime, - .tv_nsec = 0, - }, - { - .tv_sec = modificationTime, - .tv_nsec = 0, - }, - }; - if (utimensat(AT_FDCWD, path.c_str(), times, AT_SYMLINK_NOFOLLOW) == -1) - throw SysError("changing modification time of '%s' (using `utimensat`)", path); -#else - struct timeval times[2] = { - { - .tv_sec = accessedTime, - .tv_usec = 0, - }, - { - .tv_sec = modificationTime, - .tv_usec = 0, - }, - }; -#if HAVE_LUTIMES - if (lutimes(path.c_str(), times) == -1) - throw SysError("changing modification time of '%s'", path); -#else - bool isSymlink = optIsSymlink - ? *optIsSymlink - : fs::is_symlink(path); - - if (!isSymlink) { - if (utimes(path.c_str(), times) == -1) - throw SysError("changing modification time of '%s' (not a symlink)", path); - } else { - throw Error("Cannot modification time of symlink '%s'", path); - } -#endif -#endif -} - -void setWriteTime(const fs::path & path, const struct stat & st) +void setWriteTime(const std::filesystem::path & path, const struct stat & st) { setWriteTime(path, st.st_atime, st.st_mtime, S_ISLNK(st.st_mode)); } -void copyFile(const fs::path & from, const fs::path & to, bool andDelete) +void copyFile(const std::filesystem::path & from, const std::filesystem::path & to, bool andDelete) { - auto fromStatus = fs::symlink_status(from); + auto fromStatus =std::filesystem::symlink_status(from); // Mark the directory as writable so that we can delete its children - if (andDelete && fs::is_directory(fromStatus)) { - fs::permissions(from, fs::perms::owner_write, fs::perm_options::add | fs::perm_options::nofollow); + if (andDelete &&std::filesystem::is_directory(fromStatus)) { + std::filesystem::permissions(from, std::filesystem::perms::owner_write, std::filesystem::perm_options::add | std::filesystem::perm_options::nofollow); } - - if (fs::is_symlink(fromStatus) || fs::is_regular_file(fromStatus)) { - fs::copy(from, to, fs::copy_options::copy_symlinks | fs::copy_options::overwrite_existing); - } else if (fs::is_directory(fromStatus)) { - fs::create_directory(to); - for (auto & entry : fs::directory_iterator(from)) { + if (std::filesystem::is_symlink(fromStatus) ||std::filesystem::is_regular_file(fromStatus)) { + std::filesystem::copy(from, to, std::filesystem::copy_options::copy_symlinks | std::filesystem::copy_options::overwrite_existing); + } else if (std::filesystem::is_directory(fromStatus)) { + std::filesystem::create_directory(to); + for (auto & entry : DirectoryIterator(from)) { copyFile(entry, to / entry.path().filename(), andDelete); } } else { - throw Error("file '%s' has an unsupported type", from); + throw Error("file %s has an unsupported type", from); } setWriteTime(to, lstat(from.string().c_str())); if (andDelete) { - if (!fs::is_symlink(fromStatus)) - fs::permissions(from, fs::perms::owner_write, fs::perm_options::add | fs::perm_options::nofollow); - fs::remove(from); + if (!std::filesystem::is_symlink(fromStatus)) + std::filesystem::permissions(from, std::filesystem::perms::owner_write, std::filesystem::perm_options::add | std::filesystem::perm_options::nofollow); + std::filesystem::remove(from); } } @@ -728,19 +776,19 @@ void moveFile(const Path & oldName, const Path & newName) { try { std::filesystem::rename(oldName, newName); - } catch (fs::filesystem_error & e) { - auto oldPath = fs::path(oldName); - auto newPath = fs::path(newName); + } catch (std::filesystem::filesystem_error & e) { + auto oldPath = std::filesystem::path(oldName); + auto newPath = std::filesystem::path(newName); // For the move to be as atomic as possible, copy to a temporary // directory - fs::path temp = createTempDir( + std::filesystem::path temp = createTempDir( os_string_to_string(PathViewNG { newPath.parent_path() }), "rename-tmp"); - Finally removeTemp = [&]() { fs::remove(temp); }; + Finally removeTemp = [&]() { std::filesystem::remove(temp); }; auto tempCopyTarget = temp / "copy-target"; if (e.code().value() == EXDEV) { - fs::remove(newPath); - warn("Can’t rename %s as %s, copying instead", oldName, newName); + std::filesystem::remove(newPath); + warn("can’t rename %s as %s, copying instead", oldName, newName); copyFile(oldPath, tempCopyTarget, true); std::filesystem::rename( os_string_to_string(PathViewNG { tempCopyTarget }), @@ -751,7 +799,7 @@ void moveFile(const Path & oldName, const Path & newName) ////////////////////////////////////////////////////////////////////// -bool isExecutableFileAmbient(const fs::path & exe) { +bool isExecutableFileAmbient(const std::filesystem::path & exe) { // Check file type, because directory being executable means // something completely different. // `is_regular_file` follows symlinks before checking. @@ -765,4 +813,33 @@ bool isExecutableFileAmbient(const fs::path & exe) { ) == 0; } +std::filesystem::path makeParentCanonical(const std::filesystem::path & rawPath) +{ + std::filesystem::path path(absPath(rawPath));; + try { + auto parent = path.parent_path(); + if (parent == path) { + // `path` is a root directory => trivially canonical + return parent; + } + return std::filesystem::canonical(parent) / path.filename(); + } catch (std::filesystem::filesystem_error & e) { + throw SysError("canonicalising parent path of '%1%'", path); + } +} + +bool chmodIfNeeded(const std::filesystem::path & path, mode_t mode, mode_t mask) +{ + auto pathString = path.string(); + auto prevMode = lstat(pathString).st_mode; + + if (((prevMode ^ mode) & mask) == 0) + return false; + + if (chmod(pathString.c_str(), mode) != 0) + throw SysError("could not set permissions on '%s' to %o", pathString, mode); + + return true; } + +} // namespace nix diff --git a/src/libutil/freebsd/freebsd-jail.cc b/src/libutil/freebsd/freebsd-jail.cc new file mode 100644 index 00000000000..575f9287e82 --- /dev/null +++ b/src/libutil/freebsd/freebsd-jail.cc @@ -0,0 +1,52 @@ +#ifdef __FreeBSD__ +# include "nix/util/freebsd-jail.hh" + +# include +# include +# include +# include + +# include "nix/util/error.hh" +# include "nix/util/util.hh" + +namespace nix { + +AutoRemoveJail::AutoRemoveJail() + : del{false} +{ +} + +AutoRemoveJail::AutoRemoveJail(int jid) + : jid(jid) + , del(true) +{ +} + +AutoRemoveJail::~AutoRemoveJail() +{ + try { + if (del) { + if (jail_remove(jid) < 0) { + throw SysError("Failed to remove jail %1%", jid); + } + } + } catch (...) { + ignoreExceptionInDestructor(); + } +} + +void AutoRemoveJail::cancel() +{ + del = false; +} + +void AutoRemoveJail::reset(int j) +{ + del = true; + jid = j; +} + +////////////////////////////////////////////////////////////////////// + +} +#endif diff --git a/src/libutil/freebsd/include/nix/util/freebsd-jail.hh b/src/libutil/freebsd/include/nix/util/freebsd-jail.hh new file mode 100644 index 00000000000..cb5abc511a5 --- /dev/null +++ b/src/libutil/freebsd/include/nix/util/freebsd-jail.hh @@ -0,0 +1,20 @@ +#pragma once +///@file + +#include "nix/util/types.hh" + +namespace nix { + +class AutoRemoveJail +{ + int jid; + bool del; +public: + AutoRemoveJail(int jid); + AutoRemoveJail(); + ~AutoRemoveJail(); + void cancel(); + void reset(int j); +}; + +} diff --git a/src/libutil/freebsd/include/nix/util/meson.build b/src/libutil/freebsd/include/nix/util/meson.build new file mode 100644 index 00000000000..4b7d78624d9 --- /dev/null +++ b/src/libutil/freebsd/include/nix/util/meson.build @@ -0,0 +1,8 @@ +# Public headers directory + +include_dirs += include_directories('../..') + +headers += files( + 'freebsd-jail.hh', + # hack for trailing newline +) diff --git a/src/libutil/freebsd/meson.build b/src/libutil/freebsd/meson.build new file mode 100644 index 00000000000..d9b91a03d8a --- /dev/null +++ b/src/libutil/freebsd/meson.build @@ -0,0 +1,6 @@ +sources += files( + 'freebsd-jail.cc', + # hack for trailing newline +) + +subdir('include/nix/util') diff --git a/src/libutil/fs-sink.cc b/src/libutil/fs-sink.cc index 72e5c731f05..7b8fc3b2a31 100644 --- a/src/libutil/fs-sink.cc +++ b/src/libutil/fs-sink.cc @@ -1,15 +1,17 @@ #include -#include "error.hh" -#include "config-global.hh" -#include "fs-sink.hh" +#include "nix/util/error.hh" +#include "nix/util/config-global.hh" +#include "nix/util/fs-sink.hh" -#if _WIN32 +#ifdef _WIN32 # include -# include "file-path.hh" -# include "windows-error.hh" +# include "nix/util/file-path.hh" +# include "nix/util/windows-error.hh" #endif +#include "util-config-private.hh" + namespace nix { void copyRecursive( @@ -49,11 +51,13 @@ void copyRecursive( break; } - case SourceAccessor::tMisc: - throw Error("file '%1%' has an unsupported type", from); - + case SourceAccessor::tChar: + case SourceAccessor::tBlock: + case SourceAccessor::tSocket: + case SourceAccessor::tFifo: + case SourceAccessor::tUnknown: default: - unreachable(); + throw Error("file '%1%' has an unsupported type of %2%", from, stat.typeString()); } } @@ -110,7 +114,7 @@ void RestoreSink::createRegularFile(const CanonPath & path, std::function #include // for strcasecmp -#include "signals.hh" -#include "config.hh" -#include "hash.hh" +#include "nix/util/signals.hh" +#include "nix/util/configuration.hh" +#include "nix/util/hash.hh" -#include "git.hh" -#include "serialise.hh" +#include "nix/util/git.hh" +#include "nix/util/serialise.hh" namespace nix::git { @@ -33,7 +33,7 @@ std::optional decodeMode(RawMode m) { static std::string getStringUntil(Source & source, char byte) { std::string s; - char n[1]; + char n[1] = { 0 }; source(std::string_view { n, 1 }); while (*n != byte) { s += *n; @@ -134,7 +134,7 @@ void parseTree( RawMode rawMode = std::stoi(perms, 0, 8); auto modeOpt = decodeMode(rawMode); if (!modeOpt) - throw Error("Unknown Git permission: %o", perms); + throw Error("Unknown Git permission: %o", rawMode); auto mode = std::move(*modeOpt); std::string name = getStringUntil(source, '\0'); @@ -200,7 +200,11 @@ std::optional convertMode(SourceAccessor::Type type) case SourceAccessor::tSymlink: return Mode::Symlink; case SourceAccessor::tRegular: return Mode::Regular; case SourceAccessor::tDirectory: return Mode::Directory; - case SourceAccessor::tMisc: return std::nullopt; + case SourceAccessor::tChar: + case SourceAccessor::tBlock: + case SourceAccessor::tSocket: + case SourceAccessor::tFifo: return std::nullopt; + case SourceAccessor::tUnknown: default: unreachable(); } } @@ -314,9 +318,13 @@ Mode dump( return Mode::Symlink; } - case SourceAccessor::tMisc: + case SourceAccessor::tChar: + case SourceAccessor::tBlock: + case SourceAccessor::tSocket: + case SourceAccessor::tFifo: + case SourceAccessor::tUnknown: default: - throw Error("file '%1%' has an unsupported type", path); + throw Error("file '%1%' has an unsupported type of %2%", path, st.typeString()); } } diff --git a/src/libutil/hash.cc b/src/libutil/hash.cc index 748176d3370..319eb795e6b 100644 --- a/src/libutil/hash.cc +++ b/src/libutil/hash.cc @@ -1,14 +1,16 @@ #include #include +#include #include #include #include -#include "args.hh" -#include "hash.hh" -#include "archive.hh" -#include "split.hh" +#include "nix/util/args.hh" +#include "nix/util/hash.hh" +#include "nix/util/archive.hh" +#include "nix/util/configuration.hh" +#include "nix/util/split.hh" #include #include @@ -20,6 +22,7 @@ namespace nix { static size_t regularHashSize(HashAlgorithm type) { switch (type) { + case HashAlgorithm::BLAKE3: return blake3HashSize; case HashAlgorithm::MD5: return md5HashSize; case HashAlgorithm::SHA1: return sha1HashSize; case HashAlgorithm::SHA256: return sha256HashSize; @@ -29,12 +32,15 @@ static size_t regularHashSize(HashAlgorithm type) { } -const std::set hashAlgorithms = {"md5", "sha1", "sha256", "sha512" }; +const StringSet hashAlgorithms = {"blake3", "md5", "sha1", "sha256", "sha512" }; -const std::set hashFormats = {"base64", "nix32", "base16", "sri" }; +const StringSet hashFormats = {"base64", "nix32", "base16", "sri" }; -Hash::Hash(HashAlgorithm algo) : algo(algo) +Hash::Hash(HashAlgorithm algo, const ExperimentalFeatureSettings & xpSettings) : algo(algo) { + if (algo == HashAlgorithm::BLAKE3) { + xpSettings.require(Xp::BLAKE3Hashes); + } hashSize = regularHashSize(algo); assert(hashSize <= maxHashSize); memset(hash, 0, maxHashSize); @@ -134,10 +140,11 @@ std::string Hash::to_string(HashFormat hashFormat, bool includeAlgo) const Hash Hash::dummy(HashAlgorithm::SHA256); -Hash Hash::parseSRI(std::string_view original) { +Hash Hash::parseSRI(std::string_view original) +{ auto rest = original; - // Parse the has type before the separater, if there was one. + // Parse the has type before the separator, if there was one. auto hashRaw = splitPrefixTo(rest, '-'); if (!hashRaw) throw BadHash("hash '%s' is not SRI", original); @@ -283,6 +290,7 @@ Hash newHashAllowEmpty(std::string_view hashStr, std::optional ha union Ctx { + blake3_hasher blake3; MD5_CTX md5; SHA_CTX sha1; SHA256_CTX sha256; @@ -292,17 +300,39 @@ union Ctx static void start(HashAlgorithm ha, Ctx & ctx) { - if (ha == HashAlgorithm::MD5) MD5_Init(&ctx.md5); + if (ha == HashAlgorithm::BLAKE3) blake3_hasher_init(&ctx.blake3); + else if (ha == HashAlgorithm::MD5) MD5_Init(&ctx.md5); else if (ha == HashAlgorithm::SHA1) SHA1_Init(&ctx.sha1); else if (ha == HashAlgorithm::SHA256) SHA256_Init(&ctx.sha256); else if (ha == HashAlgorithm::SHA512) SHA512_Init(&ctx.sha512); } +// BLAKE3 data size threshold beyond which parallel hashing with TBB is likely faster. +// +// NOTE: This threshold is based on the recommended rule-of-thumb from the official BLAKE3 documentation for typical +// x86_64 hardware as of 2025. In the future it may make sense to allow the user to tune this through nix.conf. +const size_t blake3TbbThreshold = 128000; + +// Decide which BLAKE3 update strategy to use based on some heuristics. Currently this just checks the data size but in +// the future it might also take into consideration available system resources or the presence of a shared-memory +// capable GPU for a heterogenous compute implementation. +void blake3_hasher_update_with_heuristics(blake3_hasher * blake3, std::string_view data) +{ +#ifdef BLAKE3_USE_TBB + if (data.size() >= blake3TbbThreshold) { + blake3_hasher_update_tbb(blake3, data.data(), data.size()); + } else +#endif + { + blake3_hasher_update(blake3, data.data(), data.size()); + } +} static void update(HashAlgorithm ha, Ctx & ctx, std::string_view data) { - if (ha == HashAlgorithm::MD5) MD5_Update(&ctx.md5, data.data(), data.size()); + if (ha == HashAlgorithm::BLAKE3) blake3_hasher_update_with_heuristics(&ctx.blake3, data); + else if (ha == HashAlgorithm::MD5) MD5_Update(&ctx.md5, data.data(), data.size()); else if (ha == HashAlgorithm::SHA1) SHA1_Update(&ctx.sha1, data.data(), data.size()); else if (ha == HashAlgorithm::SHA256) SHA256_Update(&ctx.sha256, data.data(), data.size()); else if (ha == HashAlgorithm::SHA512) SHA512_Update(&ctx.sha512, data.data(), data.size()); @@ -311,24 +341,24 @@ static void update(HashAlgorithm ha, Ctx & ctx, static void finish(HashAlgorithm ha, Ctx & ctx, unsigned char * hash) { - if (ha == HashAlgorithm::MD5) MD5_Final(hash, &ctx.md5); + if (ha == HashAlgorithm::BLAKE3) blake3_hasher_finalize(&ctx.blake3, hash, BLAKE3_OUT_LEN); + else if (ha == HashAlgorithm::MD5) MD5_Final(hash, &ctx.md5); else if (ha == HashAlgorithm::SHA1) SHA1_Final(hash, &ctx.sha1); else if (ha == HashAlgorithm::SHA256) SHA256_Final(hash, &ctx.sha256); else if (ha == HashAlgorithm::SHA512) SHA512_Final(hash, &ctx.sha512); } - -Hash hashString(HashAlgorithm ha, std::string_view s) +Hash hashString( + HashAlgorithm ha, std::string_view s, const ExperimentalFeatureSettings & xpSettings) { Ctx ctx; - Hash hash(ha); + Hash hash(ha, xpSettings); start(ha, ctx); update(ha, ctx, s); finish(ha, ctx, hash.hash); return hash; } - Hash hashFile(HashAlgorithm ha, const Path & path) { HashSink sink(ha); @@ -425,6 +455,7 @@ std::string_view printHashFormat(HashFormat HashFormat) std::optional parseHashAlgoOpt(std::string_view s) { + if (s == "blake3") return HashAlgorithm::BLAKE3; if (s == "md5") return HashAlgorithm::MD5; if (s == "sha1") return HashAlgorithm::SHA1; if (s == "sha256") return HashAlgorithm::SHA256; @@ -438,12 +469,13 @@ HashAlgorithm parseHashAlgo(std::string_view s) if (opt_h) return *opt_h; else - throw UsageError("unknown hash algorithm '%1%', expect 'md5', 'sha1', 'sha256', or 'sha512'", s); + throw UsageError("unknown hash algorithm '%1%', expect 'blake3', 'md5', 'sha1', 'sha256', or 'sha512'", s); } std::string_view printHashAlgo(HashAlgorithm ha) { switch (ha) { + case HashAlgorithm::BLAKE3: return "blake3"; case HashAlgorithm::MD5: return "md5"; case HashAlgorithm::SHA1: return "sha1"; case HashAlgorithm::SHA256: return "sha256"; diff --git a/src/libutil/hilite.cc b/src/libutil/hilite.cc index e5088230d7c..6d4eb17a1ab 100644 --- a/src/libutil/hilite.cc +++ b/src/libutil/hilite.cc @@ -1,4 +1,4 @@ -#include "hilite.hh" +#include "nix/util/hilite.hh" namespace nix { @@ -23,7 +23,7 @@ std::string hiliteMatches( auto m = *it; size_t start = m.position(); out.append(s.substr(last_end, m.position() - last_end)); - // Merge continous matches + // Merge continuous matches ssize_t end = start + m.length(); while (++it != matches.end() && (*it).position() <= end) { auto n = *it; diff --git a/src/libutil/abstract-setting-to-json.hh b/src/libutil/include/nix/util/abstract-setting-to-json.hh similarity index 83% rename from src/libutil/abstract-setting-to-json.hh rename to src/libutil/include/nix/util/abstract-setting-to-json.hh index eea687d8a4a..2848f8afe4f 100644 --- a/src/libutil/abstract-setting-to-json.hh +++ b/src/libutil/include/nix/util/abstract-setting-to-json.hh @@ -2,8 +2,8 @@ ///@file #include -#include "config.hh" -#include "json-utils.hh" +#include "nix/util/configuration.hh" +#include "nix/util/json-utils.hh" namespace nix { template diff --git a/src/libutil/ansicolor.hh b/src/libutil/include/nix/util/ansicolor.hh similarity index 100% rename from src/libutil/ansicolor.hh rename to src/libutil/include/nix/util/ansicolor.hh diff --git a/src/libutil/archive.hh b/src/libutil/include/nix/util/archive.hh similarity index 95% rename from src/libutil/archive.hh rename to src/libutil/include/nix/util/archive.hh index c38fa8a46bd..ae3274fa68b 100644 --- a/src/libutil/archive.hh +++ b/src/libutil/include/nix/util/archive.hh @@ -1,9 +1,9 @@ #pragma once ///@file -#include "types.hh" -#include "serialise.hh" -#include "fs-sink.hh" +#include "nix/util/types.hh" +#include "nix/util/serialise.hh" +#include "nix/util/fs-sink.hh" namespace nix { diff --git a/src/libutil/args.hh b/src/libutil/include/nix/util/args.hh similarity index 93% rename from src/libutil/args.hh rename to src/libutil/include/nix/util/args.hh index c30d6cef8d7..f3ab0b53249 100644 --- a/src/libutil/args.hh +++ b/src/libutil/include/nix/util/args.hh @@ -9,9 +9,9 @@ #include -#include "types.hh" -#include "experimental-features.hh" -#include "ref.hh" +#include "nix/util/types.hh" +#include "nix/util/experimental-features.hh" +#include "nix/util/ref.hh" namespace nix { @@ -179,7 +179,7 @@ public: using ptr = std::shared_ptr; std::string longName; - std::set aliases; + StringSet aliases; char shortName = 0; std::string description; std::string category; @@ -252,7 +252,7 @@ protected: std::list processedArgs; /** - * Process some positional arugments + * Process some positional arguments * * @param finish: We have parsed everything else, and these are the only * arguments left. Used because we accumulate some "pending args" we might @@ -263,7 +263,7 @@ protected: virtual Strings::iterator rewriteArgs(Strings & args, Strings::iterator pos) { return pos; } - std::set hiddenCategories; + StringSet hiddenCategories; /** * Called after all command line flags before the first non-flag @@ -393,8 +393,30 @@ public: nlohmann::json toJSON() override; + enum struct AliasStatus { + /** Aliases that don't go away */ + AcceptedShorthand, + /** Aliases that will go away */ + Deprecated, + }; + + /** An alias, except for the original syntax, which is in the map key. */ + struct AliasInfo { + AliasStatus status; + std::vector replacement; + }; + + /** + * A list of aliases (remapping a deprecated/shorthand subcommand + * to something else). + */ + std::map aliases; + + Strings::iterator rewriteArgs(Strings & args, Strings::iterator pos) override; + protected: std::string commandName = ""; + bool aliasUsed = false; }; Strings argvToStrings(int argc, char * * argv); diff --git a/src/libutil/args/root.hh b/src/libutil/include/nix/util/args/root.hh similarity index 98% rename from src/libutil/args/root.hh rename to src/libutil/include/nix/util/args/root.hh index 34a43b53835..cdc9be61331 100644 --- a/src/libutil/args/root.hh +++ b/src/libutil/include/nix/util/args/root.hh @@ -1,6 +1,6 @@ #pragma once -#include "args.hh" +#include "nix/util/args.hh" namespace nix { diff --git a/src/libutil/callback.hh b/src/libutil/include/nix/util/callback.hh similarity index 98% rename from src/libutil/callback.hh rename to src/libutil/include/nix/util/callback.hh index 26c386d8078..c2cada2f682 100644 --- a/src/libutil/callback.hh +++ b/src/libutil/include/nix/util/callback.hh @@ -1,6 +1,7 @@ #pragma once ///@file +#include #include #include diff --git a/src/libutil/canon-path.hh b/src/libutil/include/nix/util/canon-path.hh similarity index 100% rename from src/libutil/canon-path.hh rename to src/libutil/include/nix/util/canon-path.hh diff --git a/src/libutil/checked-arithmetic.hh b/src/libutil/include/nix/util/checked-arithmetic.hh similarity index 100% rename from src/libutil/checked-arithmetic.hh rename to src/libutil/include/nix/util/checked-arithmetic.hh diff --git a/src/libutil/chunked-vector.hh b/src/libutil/include/nix/util/chunked-vector.hh similarity index 79% rename from src/libutil/chunked-vector.hh rename to src/libutil/include/nix/util/chunked-vector.hh index 4709679a62a..2c21183ac1e 100644 --- a/src/libutil/chunked-vector.hh +++ b/src/libutil/include/nix/util/chunked-vector.hh @@ -6,7 +6,7 @@ #include #include -#include "error.hh" +#include "nix/util/error.hh" namespace nix { @@ -45,9 +45,10 @@ public: addChunk(); } - uint32_t size() const { return size_; } + uint32_t size() const noexcept { return size_; } - std::pair add(T value) + template + std::pair add(Args &&... args) { const auto idx = size_++; auto & chunk = [&] () -> auto & { @@ -55,11 +56,16 @@ public: return back; return addChunk(); }(); - auto & result = chunk.emplace_back(std::move(value)); + auto & result = chunk.emplace_back(std::forward(args)...); return {result, idx}; } - const T & operator[](uint32_t idx) const + /** + * Unchecked subscript operator. + * @pre add must have been called at least idx + 1 times. + * @throws nothing + */ + const T & operator[](uint32_t idx) const noexcept { return chunks[idx / ChunkSize][idx % ChunkSize]; } diff --git a/src/libutil/closure.hh b/src/libutil/include/nix/util/closure.hh similarity index 98% rename from src/libutil/closure.hh rename to src/libutil/include/nix/util/closure.hh index 16e3b93e488..54b18ab3dbe 100644 --- a/src/libutil/closure.hh +++ b/src/libutil/include/nix/util/closure.hh @@ -3,7 +3,7 @@ #include #include -#include "sync.hh" +#include "nix/util/sync.hh" using std::set; diff --git a/src/libutil/comparator.hh b/src/libutil/include/nix/util/comparator.hh similarity index 97% rename from src/libutil/comparator.hh rename to src/libutil/include/nix/util/comparator.hh index 34ba6f4534c..c3af1758dff 100644 --- a/src/libutil/comparator.hh +++ b/src/libutil/include/nix/util/comparator.hh @@ -16,7 +16,7 @@ /** * Awful hacky generation of the comparison operators by doing a lexicographic - * comparison between the choosen fields. + * comparison between the chosen fields. * * ``` * GENERATE_CMP(ClassName, me->field1, me->field2, ...) diff --git a/src/libutil/compression.hh b/src/libutil/include/nix/util/compression.hh similarity index 89% rename from src/libutil/compression.hh rename to src/libutil/include/nix/util/compression.hh index e0c531b1f38..15d869e88f0 100644 --- a/src/libutil/compression.hh +++ b/src/libutil/include/nix/util/compression.hh @@ -1,9 +1,9 @@ #pragma once ///@file -#include "ref.hh" -#include "types.hh" -#include "serialise.hh" +#include "nix/util/ref.hh" +#include "nix/util/types.hh" +#include "nix/util/serialise.hh" #include diff --git a/src/libutil/compute-levels.hh b/src/libutil/include/nix/util/compute-levels.hh similarity index 71% rename from src/libutil/compute-levels.hh rename to src/libutil/include/nix/util/compute-levels.hh index 093e7a915a4..4015477939a 100644 --- a/src/libutil/compute-levels.hh +++ b/src/libutil/include/nix/util/compute-levels.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "types.hh" +#include "nix/util/types.hh" namespace nix { diff --git a/src/libutil/config-global.hh b/src/libutil/include/nix/util/config-global.hh similarity index 76% rename from src/libutil/config-global.hh rename to src/libutil/include/nix/util/config-global.hh index 2caf515240d..44f89e06df5 100644 --- a/src/libutil/config-global.hh +++ b/src/libutil/include/nix/util/config-global.hh @@ -1,14 +1,19 @@ #pragma once ///@file -#include "config.hh" +#include "nix/util/configuration.hh" namespace nix { struct GlobalConfig : public AbstractConfig { typedef std::vector ConfigRegistrations; - static ConfigRegistrations * configRegistrations; + + static ConfigRegistrations & configRegistrations() + { + static ConfigRegistrations configRegistrations; + return configRegistrations; + } bool set(const std::string & name, const std::string & value) override; diff --git a/src/libutil/config-impl.hh b/src/libutil/include/nix/util/config-impl.hh similarity index 98% rename from src/libutil/config-impl.hh rename to src/libutil/include/nix/util/config-impl.hh index c3aa61ddb95..15e0c955483 100644 --- a/src/libutil/config-impl.hh +++ b/src/libutil/include/nix/util/config-impl.hh @@ -12,7 +12,8 @@ * instantiation. */ -#include "config.hh" +#include "nix/util/configuration.hh" +#include "nix/util/args.hh" namespace nix { diff --git a/src/libutil/config.hh b/src/libutil/include/nix/util/configuration.hh similarity index 96% rename from src/libutil/config.hh rename to src/libutil/include/nix/util/configuration.hh index e98e09bf76c..24b42f02c84 100644 --- a/src/libutil/config.hh +++ b/src/libutil/include/nix/util/configuration.hh @@ -7,8 +7,8 @@ #include -#include "types.hh" -#include "experimental-features.hh" +#include "nix/util/types.hh" +#include "nix/util/experimental-features.hh" namespace nix { @@ -179,7 +179,7 @@ public: const std::string name; const std::string description; - const std::set aliases; + const StringSet aliases; int created = 123; @@ -192,7 +192,7 @@ protected: AbstractSetting( const std::string & name, const std::string & description, - const std::set & aliases, + const StringSet & aliases, std::optional experimentalFeature = std::nullopt); virtual ~AbstractSetting(); @@ -251,7 +251,7 @@ public: const bool documentDefault, const std::string & name, const std::string & description, - const std::set & aliases = {}, + const StringSet & aliases = {}, std::optional experimentalFeature = std::nullopt) : AbstractSetting(name, description, aliases, experimentalFeature) , value(def) @@ -262,6 +262,7 @@ public: operator const T &() const { return value; } operator T &() { return value; } const T & get() const { return value; } + T & get() { return value; } template bool operator ==(const U & v2) const { return value == v2; } template @@ -322,7 +323,7 @@ public: const T & def, const std::string & name, const std::string & description, - const std::set & aliases = {}, + const StringSet & aliases = {}, const bool documentDefault = true, std::optional experimentalFeature = std::nullopt) : BaseSetting(def, documentDefault, name, description, aliases, std::move(experimentalFeature)) @@ -348,7 +349,7 @@ public: const Path & def, const std::string & name, const std::string & description, - const std::set & aliases = {}); + const StringSet & aliases = {}); Path parse(const std::string & str) const override; @@ -370,7 +371,7 @@ public: const std::optional & def, const std::string & name, const std::string & description, - const std::set & aliases = {}); + const StringSet & aliases = {}); std::optional parse(const std::string & str) const override; diff --git a/src/libutil/current-process.hh b/src/libutil/include/nix/util/current-process.hh similarity index 84% rename from src/libutil/current-process.hh rename to src/libutil/include/nix/util/current-process.hh index 8286bf89d66..b2c92a34ca6 100644 --- a/src/libutil/current-process.hh +++ b/src/libutil/include/nix/util/current-process.hh @@ -7,7 +7,7 @@ # include #endif -#include "types.hh" +#include "nix/util/types.hh" namespace nix { @@ -17,10 +17,13 @@ namespace nix { */ unsigned int getMaxCPU(); +// It does not seem possible to dynamically change stack size on Windows. +#ifndef _WIN32 /** * Change the stack size. */ void setStackSize(size_t stackSize); +#endif /** * Restore the original inherited Unix process context (such as signal diff --git a/src/libutil/english.hh b/src/libutil/include/nix/util/english.hh similarity index 100% rename from src/libutil/english.hh rename to src/libutil/include/nix/util/english.hh diff --git a/src/libutil/environment-variables.hh b/src/libutil/include/nix/util/environment-variables.hh similarity index 88% rename from src/libutil/environment-variables.hh rename to src/libutil/include/nix/util/environment-variables.hh index 1a95f5c97e7..9b2fab4f487 100644 --- a/src/libutil/environment-variables.hh +++ b/src/libutil/include/nix/util/environment-variables.hh @@ -8,8 +8,8 @@ #include -#include "types.hh" -#include "file-path.hh" +#include "nix/util/types.hh" +#include "nix/util/file-path.hh" namespace nix { @@ -34,7 +34,7 @@ std::optional getEnvNonEmpty(const std::string & key); /** * Get the entire environment. */ -std::map getEnv(); +StringMap getEnv(); #ifdef _WIN32 /** @@ -64,6 +64,6 @@ void clearEnv(); /** * Replace the entire environment with the given one. */ -void replaceEnv(const std::map & newEnv); +void replaceEnv(const StringMap & newEnv); } diff --git a/src/libutil/error.hh b/src/libutil/include/nix/util/error.hh similarity index 88% rename from src/libutil/error.hh rename to src/libutil/include/nix/util/error.hh index 58d9026222f..7c96112eac4 100644 --- a/src/libutil/error.hh +++ b/src/libutil/include/nix/util/error.hh @@ -15,8 +15,8 @@ * See libutil/tests/logging.cc for usage examples. */ -#include "suggestions.hh" -#include "fmt.hh" +#include "nix/util/suggestions.hh" +#include "nix/util/fmt.hh" #include #include @@ -50,6 +50,14 @@ struct LinesOfCode { std::optional nextLineOfCode; }; +/* NOTE: position.hh recursively depends on source-path.hh -> source-accessor.hh + -> hash.hh -> configuration.hh -> experimental-features.hh -> error.hh -> Pos. + There are other such cycles. + Thus, Pos has to be an incomplete type in this header. But since ErrorInfo/Trace + have to refer to Pos, they have to use pointer indirection via std::shared_ptr + to break the recursive header dependency. + FIXME: Untangle this mess. Should there be AbstractPos as there used to be before + 4feb7d9f71? */ struct Pos; void printCodeLines(std::ostream & out, @@ -70,7 +78,7 @@ enum struct TracePrint { }; struct Trace { - std::shared_ptr pos; + std::shared_ptr pos; HintFmt hint; TracePrint print = TracePrint::Default; }; @@ -80,7 +88,7 @@ inline std::strong_ordering operator<=>(const Trace& lhs, const Trace& rhs); struct ErrorInfo { Verbosity level; HintFmt msg; - std::shared_ptr pos; + std::shared_ptr pos; std::list traces; /** * Some messages are generated directly by expressions; notably `builtins.warn`, `abort`, `throw`. @@ -164,7 +172,7 @@ public: err.status = status; } - void atPos(std::shared_ptr pos) { + void atPos(std::shared_ptr pos) { err.pos = pos; } @@ -174,12 +182,12 @@ public: } template - void addTrace(std::shared_ptr && e, std::string_view fs, const Args & ... args) + void addTrace(std::shared_ptr && e, std::string_view fs, const Args & ... args) { addTrace(std::move(e), HintFmt(std::string(fs), args...)); } - void addTrace(std::shared_ptr && e, HintFmt hint, TracePrint print = TracePrint::Default); + void addTrace(std::shared_ptr && e, HintFmt hint, TracePrint print = TracePrint::Default); bool hasTrace() const { return !err.traces.empty(); } diff --git a/src/libutil/exec.hh b/src/libutil/include/nix/util/exec.hh similarity index 89% rename from src/libutil/exec.hh rename to src/libutil/include/nix/util/exec.hh index cbbe80c4e9b..a362cef35c9 100644 --- a/src/libutil/exec.hh +++ b/src/libutil/include/nix/util/exec.hh @@ -1,6 +1,6 @@ #pragma once -#include "os-string.hh" +#include "nix/util/os-string.hh" namespace nix { diff --git a/src/libutil/executable-path.hh b/src/libutil/include/nix/util/executable-path.hh similarity index 92% rename from src/libutil/executable-path.hh rename to src/libutil/include/nix/util/executable-path.hh index c5cfa1c3918..cf6f3b25200 100644 --- a/src/libutil/executable-path.hh +++ b/src/libutil/include/nix/util/executable-path.hh @@ -1,14 +1,14 @@ #pragma once ///@file -#include "file-system.hh" +#include "nix/util/file-system.hh" namespace nix { MakeError(ExecutableLookupError, Error); /** - * @todo rename, it is not just good for execuatable paths, but also + * @todo rename, it is not just good for executable paths, but also * other lists of paths. */ struct ExecutablePath @@ -51,7 +51,7 @@ struct ExecutablePath * * @param exe This must just be a name, and not contain any `/` (or * `\` on Windows). in case it does, per the spec no lookup should - * be perfomed, and the path (it is not just a file name) as is. + * be performed, and the path (it is not just a file name) as is. * This is the caller's respsonsibility. * * This is a pure function, except for the default `isExecutable` diff --git a/src/libutil/exit.hh b/src/libutil/include/nix/util/exit.hh similarity index 100% rename from src/libutil/exit.hh rename to src/libutil/include/nix/util/exit.hh diff --git a/src/libutil/experimental-features.hh b/src/libutil/include/nix/util/experimental-features.hh similarity index 94% rename from src/libutil/experimental-features.hh rename to src/libutil/include/nix/util/experimental-features.hh index 412bf08864d..8923517bace 100644 --- a/src/libutil/experimental-features.hh +++ b/src/libutil/include/nix/util/experimental-features.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "error.hh" -#include "types.hh" +#include "nix/util/error.hh" +#include "nix/util/types.hh" #include @@ -37,6 +37,7 @@ enum struct ExperimentalFeature MountedSSHStore, VerifiedFetches, PipeOperators, + BLAKE3Hashes, }; /** @@ -75,7 +76,7 @@ std::ostream & operator<<( * Parse a set of strings to the corresponding set of experimental * features, ignoring (but warning for) any unknown feature. */ -std::set parseFeatures(const std::set &); +std::set parseFeatures(const StringSet &); /** * An experimental feature was required for some (experimental) diff --git a/src/libutil/file-content-address.hh b/src/libutil/include/nix/util/file-content-address.hh similarity index 99% rename from src/libutil/file-content-address.hh rename to src/libutil/include/nix/util/file-content-address.hh index 226068387d6..0922604f8c9 100644 --- a/src/libutil/file-content-address.hh +++ b/src/libutil/include/nix/util/file-content-address.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "source-accessor.hh" +#include "nix/util/source-accessor.hh" namespace nix { diff --git a/src/libutil/file-descriptor.hh b/src/libutil/include/nix/util/file-descriptor.hh similarity index 96% rename from src/libutil/file-descriptor.hh rename to src/libutil/include/nix/util/file-descriptor.hh index fde36299975..e2bcce2a283 100644 --- a/src/libutil/file-descriptor.hh +++ b/src/libutil/include/nix/util/file-descriptor.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "types.hh" -#include "error.hh" +#include "nix/util/types.hh" +#include "nix/util/error.hh" #ifdef _WIN32 # define WIN32_LEAN_AND_MEAN @@ -18,7 +18,7 @@ struct Source; * Operating System capability */ using Descriptor = -#if _WIN32 +#ifdef _WIN32 HANDLE #else int @@ -26,7 +26,7 @@ using Descriptor = ; const Descriptor INVALID_DESCRIPTOR = -#if _WIN32 +#ifdef _WIN32 INVALID_HANDLE_VALUE #else -1 @@ -68,7 +68,7 @@ static inline int fromDescriptorReadOnly(Descriptor fd) std::string readFile(Descriptor fd); /** - * Wrappers arount read()/write() that read/write exactly the + * Wrappers around read()/write() that read/write exactly the * requested number of bytes. */ void readFull(Descriptor fd, char * buf, size_t count); diff --git a/src/libutil/file-path-impl.hh b/src/libutil/include/nix/util/file-path-impl.hh similarity index 97% rename from src/libutil/file-path-impl.hh rename to src/libutil/include/nix/util/file-path-impl.hh index d7c823fd0f1..1b4dd28f197 100644 --- a/src/libutil/file-path-impl.hh +++ b/src/libutil/include/nix/util/file-path-impl.hh @@ -11,7 +11,7 @@ namespace nix { /** - * Unix-style path primives. + * Unix-style path primitives. * * Nix'result own "logical" paths are always Unix-style. So this is always * used for that, and additionally used for native paths on Unix. @@ -51,7 +51,7 @@ struct UnixPathTrait * often manipulating them converted to UTF-8 (*) using `char`. * * (Actually neither are guaranteed to be valid unicode; both are - * arbitrary non-0 8- or 16-bit bytes. But for charcters with specifical + * arbitrary non-0 8- or 16-bit bytes. But for characters with specifical * meaning like '/', '\\', ':', etc., we refer to an encoding scheme, * and also for sake of UIs that display paths a text.) */ diff --git a/src/libutil/file-path.hh b/src/libutil/include/nix/util/file-path.hh similarity index 93% rename from src/libutil/file-path.hh rename to src/libutil/include/nix/util/file-path.hh index 8e4a88b9d56..deff076f1f2 100644 --- a/src/libutil/file-path.hh +++ b/src/libutil/include/nix/util/file-path.hh @@ -3,8 +3,8 @@ #include -#include "types.hh" -#include "os-string.hh" +#include "nix/util/types.hh" +#include "nix/util/os-string.hh" namespace nix { diff --git a/src/libutil/file-system.hh b/src/libutil/include/nix/util/file-system.hh similarity index 62% rename from src/libutil/file-system.hh rename to src/libutil/include/nix/util/file-system.hh index 3c49181a0ad..c45cb55aa74 100644 --- a/src/libutil/file-system.hh +++ b/src/libutil/include/nix/util/file-system.hh @@ -2,14 +2,12 @@ /** * @file * - * Utiltities for working with the file sytem and file paths. + * Utilities for working with the file system and file paths. */ -#include "types.hh" -#include "error.hh" -#include "logging.hh" -#include "file-descriptor.hh" -#include "file-path.hh" +#include "nix/util/types.hh" +#include "nix/util/file-descriptor.hh" +#include "nix/util/file-path.hh" #include #include @@ -18,12 +16,8 @@ #ifdef _WIN32 # include #endif -#include -#include #include -#include -#include #include /** @@ -42,6 +36,11 @@ namespace nix { struct Sink; struct Source; +/** + * Return whether the path denotes an absolute path. + */ +bool isAbsolute(PathView path); + /** * @return An absolutized path, resolving paths relative to the * specified directory, or the current directory otherwise. The path @@ -100,13 +99,13 @@ std::string_view baseNameOf(std::string_view path); * Check whether 'path' is a descendant of 'dir'. Both paths must be * canonicalized. */ -bool isInDir(std::string_view path, std::string_view dir); +bool isInDir(const std::filesystem::path & path, const std::filesystem::path & dir); /** * Check whether 'path' is equal to 'dir' or a descendant of * 'dir'. Both paths must be canonicalized. */ -bool isDirOrInDir(std::string_view path, std::string_view dir); +bool isDirOrInDir(const std::filesystem::path & path, const std::filesystem::path & dir); /** * Get status of `path`. @@ -121,27 +120,25 @@ std::optional maybeLstat(const Path & path); /** * @return true iff the given path exists. - * - * In the process of being deprecated for `fs::symlink_exists`. */ -bool pathExists(const Path & path); - -namespace fs { +bool pathExists(const std::filesystem::path & path); /** - * ``` - * symlink_exists(p) = std::filesystem::exists(std::filesystem::symlink_status(p)) - * ``` - * Missing convenience analogous to - * ``` - * std::filesystem::exists(p) = std::filesystem::exists(std::filesystem::status(p)) - * ``` + * Canonicalize a path except for the last component. + * + * This is useful for getting the canonical location of a symlink. + * + * Consider the case where `foo/l` is a symlink. `canonical("foo/l")` will + * resolve the symlink `l` to its target. + * `makeParentCanonical("foo/l")` will not resolve the symlink `l` to its target, + * but does ensure that the returned parent part of the path, `foo` is resolved + * to `canonical("foo")`, and can therefore be retrieved without traversing any + * symlinks. + * + * If a relative path is passed, it will be made absolute, so that the parent + * can always be canonicalized. */ -inline bool symlink_exists(const std::filesystem::path & path) { - return std::filesystem::exists(std::filesystem::symlink_status(path)); -} - -} // namespace fs +std::filesystem::path makeParentCanonical(const std::filesystem::path & path); /** * A version of pathExists that returns false on a permission error. @@ -170,23 +167,29 @@ Descriptor openDirectory(const std::filesystem::path & path); */ std::string readFile(const Path & path); std::string readFile(const std::filesystem::path & path); -void readFile(const Path & path, Sink & sink); +void readFile(const Path & path, Sink & sink, bool memory_map = true); + +enum struct FsSync { Yes, No }; /** * Write a string to a file. */ -void writeFile(const Path & path, std::string_view s, mode_t mode = 0666, bool sync = false); -static inline void writeFile(const std::filesystem::path & path, std::string_view s, mode_t mode = 0666, bool sync = false) +void writeFile(const Path & path, std::string_view s, mode_t mode = 0666, FsSync sync = FsSync::No); + +static inline void writeFile(const std::filesystem::path & path, std::string_view s, mode_t mode = 0666, FsSync sync = FsSync::No) { return writeFile(path.string(), s, mode, sync); } -void writeFile(const Path & path, Source & source, mode_t mode = 0666, bool sync = false); -static inline void writeFile(const std::filesystem::path & path, Source & source, mode_t mode = 0666, bool sync = false) +void writeFile(const Path & path, Source & source, mode_t mode = 0666, FsSync sync = FsSync::No); + +static inline void writeFile(const std::filesystem::path & path, Source & source, mode_t mode = 0666, FsSync sync = FsSync::No) { return writeFile(path.string(), source, mode, sync); } +void writeFile(AutoCloseFD & fd, const Path & origPath, std::string_view s, mode_t mode = 0666, FsSync sync = FsSync::No); + /** * Flush a path's parent directory to disk. */ @@ -209,14 +212,9 @@ void deletePath(const std::filesystem::path & path, uint64_t & bytesFreed); /** * Create a directory and all its parents, if necessary. * - * In the process of being deprecated for - * `std::filesystem::create_directories`. + * Wrapper around `std::filesystem::create_directories` to handle exceptions. */ -void createDirs(const Path & path); -inline void createDirs(PathView path) -{ - return createDirs(Path(path)); -} +void createDirs(const std::filesystem::path & path); /** * Create a single directory. @@ -319,7 +317,7 @@ typedef std::unique_ptr AutoCloseDir; * Create a temporary directory. */ Path createTempDir(const Path & tmpRoot = "", const Path & prefix = "nix", - bool includePid = true, bool useGlobalCounter = true, mode_t mode = 0755); + mode_t mode = 0755); /** * Create a temporary file, returning a file handle and its path. @@ -337,6 +335,14 @@ Path defaultTempDir(); */ bool isExecutableFileAmbient(const std::filesystem::path & exe); +/** + * Return temporary path constructed by appending a suffix to a root path. + * + * The constructed path looks like `--`. To create a + * path nested in a directory, provide a suffix starting with `/`. + */ +Path makeTempPath(const Path & root, const Path & suffix = ".tmp"); + /** * Used in various places. */ @@ -344,4 +350,93 @@ typedef std::function PathFilter; extern PathFilter defaultPathFilter; +/** + * Change permissions of a file only if necessary. + * + * @details + * Skip chmod call if the directory already has the requested permissions. + * This is to avoid failing when the executing user lacks permissions to change the + * directory's permissions even if it would be no-op. + * + * @param path Path to the file to change the permissions for. + * @param mode New file mode. + * @param mask Used for checking if the file already has requested permissions. + * + * @return true if permissions changed, false otherwise. + */ +bool chmodIfNeeded(const std::filesystem::path & path, mode_t mode, mode_t mask = S_IRWXU | S_IRWXG | S_IRWXO); + +/** + * @brief A directory iterator that can be used to iterate over the + * contents of a directory. It is similar to std::filesystem::directory_iterator + * but throws NixError on failure instead of std::filesystem::filesystem_error. + */ +class DirectoryIterator { +public: + // --- Iterator Traits --- + using iterator_category = std::input_iterator_tag; + using value_type = std::filesystem::directory_entry; + using difference_type = std::ptrdiff_t; + using pointer = const std::filesystem::directory_entry*; + using reference = const std::filesystem::directory_entry&; + + // Default constructor (represents end iterator) + DirectoryIterator() noexcept = default; + + // Constructor taking a path + explicit DirectoryIterator(const std::filesystem::path& p); + + reference operator*() const { + // Accessing the value itself doesn't typically throw filesystem_error + // after successful construction/increment, but underlying operations might. + // If directory_entry methods called via -> could throw, add try-catch there. + return *it_; + } + + pointer operator->() const { + return &(*it_); + } + + + DirectoryIterator& operator++(); + + // Postfix increment operator + DirectoryIterator operator++(int) { + DirectoryIterator temp = *this; + ++(*this); // Uses the prefix increment's try-catch logic + return temp; + } + + // Equality comparison + friend bool operator==(const DirectoryIterator& a, const DirectoryIterator& b) noexcept { + return a.it_ == b.it_; + } + + // Inequality comparison + friend bool operator!=(const DirectoryIterator& a, const DirectoryIterator& b) noexcept { + return !(a == b); + } + + // Allow direct use in range-based for loops if iterating over an instance + DirectoryIterator begin() const { return *this; } + DirectoryIterator end() const { return DirectoryIterator{}; } + + +private: + std::filesystem::directory_iterator it_; +}; + +#ifdef __FreeBSD__ +class AutoUnmount +{ + Path path; + bool del; +public: + AutoUnmount(Path&); + AutoUnmount(); + ~AutoUnmount(); + void cancel(); +}; +#endif + } diff --git a/src/libutil/finally.hh b/src/libutil/include/nix/util/finally.hh similarity index 100% rename from src/libutil/finally.hh rename to src/libutil/include/nix/util/finally.hh diff --git a/src/libutil/fmt.hh b/src/libutil/include/nix/util/fmt.hh similarity index 99% rename from src/libutil/fmt.hh rename to src/libutil/include/nix/util/fmt.hh index 850b7162d87..5435a4ebf20 100644 --- a/src/libutil/fmt.hh +++ b/src/libutil/include/nix/util/fmt.hh @@ -3,7 +3,7 @@ #include #include -#include "ansicolor.hh" +#include "nix/util/ansicolor.hh" namespace nix { diff --git a/src/libutil/fs-sink.hh b/src/libutil/include/nix/util/fs-sink.hh similarity index 96% rename from src/libutil/fs-sink.hh rename to src/libutil/include/nix/util/fs-sink.hh index 5c5073731f6..1c34fba9356 100644 --- a/src/libutil/fs-sink.hh +++ b/src/libutil/include/nix/util/fs-sink.hh @@ -1,9 +1,9 @@ #pragma once ///@file -#include "serialise.hh" -#include "source-accessor.hh" -#include "file-system.hh" +#include "nix/util/serialise.hh" +#include "nix/util/source-accessor.hh" +#include "nix/util/file-system.hh" namespace nix { diff --git a/src/libutil/git.hh b/src/libutil/include/nix/util/git.hh similarity index 97% rename from src/libutil/git.hh rename to src/libutil/include/nix/util/git.hh index 1a6a7c3331b..9bdb30bb9c5 100644 --- a/src/libutil/git.hh +++ b/src/libutil/include/nix/util/git.hh @@ -5,11 +5,11 @@ #include #include -#include "types.hh" -#include "serialise.hh" -#include "hash.hh" -#include "source-path.hh" -#include "fs-sink.hh" +#include "nix/util/types.hh" +#include "nix/util/serialise.hh" +#include "nix/util/hash.hh" +#include "nix/util/source-path.hh" +#include "nix/util/fs-sink.hh" namespace nix::git { diff --git a/src/libutil/hash.hh b/src/libutil/include/nix/util/hash.hh similarity index 89% rename from src/libutil/hash.hh rename to src/libutil/include/nix/util/hash.hh index dc95b9f2f9b..71553745662 100644 --- a/src/libutil/hash.hh +++ b/src/libutil/include/nix/util/hash.hh @@ -1,9 +1,10 @@ #pragma once ///@file -#include "types.hh" -#include "serialise.hh" -#include "file-system.hh" +#include "nix/util/configuration.hh" +#include "nix/util/types.hh" +#include "nix/util/serialise.hh" +#include "nix/util/file-system.hh" namespace nix { @@ -11,15 +12,15 @@ namespace nix { MakeError(BadHash, Error); -enum struct HashAlgorithm : char { MD5 = 42, SHA1, SHA256, SHA512 }; - +enum struct HashAlgorithm : char { MD5 = 42, SHA1, SHA256, SHA512, BLAKE3 }; +const int blake3HashSize = 32; const int md5HashSize = 16; const int sha1HashSize = 20; const int sha256HashSize = 32; const int sha512HashSize = 64; -extern const std::set hashAlgorithms; +extern const StringSet hashAlgorithms; extern const std::string nix32Chars; @@ -35,11 +36,11 @@ enum struct HashFormat : int { /// @brief Lowercase hexadecimal encoding. @see base16Chars Base16, /// @brief ":", format of the SRI integrity attribute. - /// @see W3C recommendation [Subresource Intergrity](https://www.w3.org/TR/SRI/). + /// @see W3C recommendation [Subresource Integrity](https://www.w3.org/TR/SRI/). SRI }; -extern const std::set hashFormats; +extern const StringSet hashFormats; struct Hash { @@ -52,7 +53,7 @@ struct Hash /** * Create a zero-filled hash object. */ - explicit Hash(HashAlgorithm algo); + explicit Hash(HashAlgorithm algo, const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); /** * Parse the hash from a string representation in the format @@ -65,7 +66,7 @@ struct Hash /** * Parse a hash from a string representation like the above, except the - * type prefix is mandatory is there is no separate arguement. + * type prefix is mandatory is there is no separate argument. */ static Hash parseAnyPrefixed(std::string_view s); @@ -157,7 +158,7 @@ std::string printHash16or32(const Hash & hash); /** * Compute the hash of the given string. */ -Hash hashString(HashAlgorithm ha, std::string_view s); +Hash hashString(HashAlgorithm ha, std::string_view s, const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); /** * Compute the hash of the given file, hashing its contents directly. diff --git a/src/libutil/hilite.hh b/src/libutil/include/nix/util/hilite.hh similarity index 100% rename from src/libutil/hilite.hh rename to src/libutil/include/nix/util/hilite.hh diff --git a/src/libutil/json-impls.hh b/src/libutil/include/nix/util/json-impls.hh similarity index 95% rename from src/libutil/json-impls.hh rename to src/libutil/include/nix/util/json-impls.hh index b26163a04ae..9dd344c508d 100644 --- a/src/libutil/json-impls.hh +++ b/src/libutil/include/nix/util/json-impls.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "nlohmann/json_fwd.hpp" +#include // Following https://github.com/nlohmann/json#how-can-i-use-get-for-non-default-constructiblenon-copyable-types #define JSON_IMPL(TYPE) \ diff --git a/src/libutil/json-utils.hh b/src/libutil/include/nix/util/json-utils.hh similarity index 73% rename from src/libutil/json-utils.hh rename to src/libutil/include/nix/util/json-utils.hh index 546334e1e1f..37f4d58f89a 100644 --- a/src/libutil/json-utils.hh +++ b/src/libutil/include/nix/util/json-utils.hh @@ -4,7 +4,8 @@ #include #include -#include "types.hh" +#include "nix/util/error.hh" +#include "nix/util/types.hh" namespace nix { @@ -25,6 +26,7 @@ const nlohmann::json & valueAt( const std::string & key); std::optional optionalValueAt(const nlohmann::json::object_t & value, const std::string & key); +std::optional nullableValueAt(const nlohmann::json::object_t & value, const std::string & key); /** * Downcast the json object, failing with a nice error if the conversion fails. @@ -34,7 +36,26 @@ const nlohmann::json * getNullable(const nlohmann::json & value); const nlohmann::json::object_t & getObject(const nlohmann::json & value); const nlohmann::json::array_t & getArray(const nlohmann::json & value); const nlohmann::json::string_t & getString(const nlohmann::json & value); -const nlohmann::json::number_integer_t & getInteger(const nlohmann::json & value); +const nlohmann::json::number_unsigned_t & getUnsigned(const nlohmann::json & value); + +template +auto getInteger(const nlohmann::json & value) -> std::enable_if_t && std::is_integral_v, T> +{ + if (auto ptr = value.get_ptr()) { + if (*ptr <= std::make_unsigned_t(std::numeric_limits::max())) { + return *ptr; + } + } else if (auto ptr = value.get_ptr()) { + if (*ptr >= std::numeric_limits::min() && *ptr <= std::numeric_limits::max()) { + return *ptr; + } + } else { + auto typeName = value.is_number_float() ? "floating point number" : value.type_name(); + throw Error("Expected JSON value to be an integral number but it is of type '%s': %s", typeName, value.dump()); + } + throw Error("Out of range: JSON value '%s' cannot be casted to %d-bit integer", value.dump(), 8 * sizeof(T)); +} + const nlohmann::json::boolean_t & getBoolean(const nlohmann::json & value); Strings getStringList(const nlohmann::json & value); StringMap getStringMap(const nlohmann::json & value); @@ -69,6 +90,9 @@ struct json_avoids_null> : std::true_type {}; template struct json_avoids_null> : std::true_type {}; +template +struct json_avoids_null> : std::true_type {}; + template struct json_avoids_null> : std::true_type {}; diff --git a/src/libutil/logging.hh b/src/libutil/include/nix/util/logging.hh similarity index 77% rename from src/libutil/logging.hh rename to src/libutil/include/nix/util/logging.hh index 250f9209972..dabfac48390 100644 --- a/src/libutil/logging.hh +++ b/src/libutil/include/nix/util/logging.hh @@ -1,8 +1,12 @@ #pragma once ///@file -#include "error.hh" -#include "config.hh" +#include "nix/util/error.hh" +#include "nix/util/configuration.hh" +#include "nix/util/file-descriptor.hh" +#include "nix/util/finally.hh" + +#include #include @@ -47,6 +51,16 @@ struct LoggerSettings : Config Whether Nix should print out a stack trace in case of Nix expression evaluation errors. )"}; + + Setting jsonLogPath{ + this, "", "json-log-path", + R"( + A file or unix socket to which JSON records of Nix's log output are + written, in the same format as `--log-format internal-json` + (without the `@nix ` prefixes on each line). + Concurrent writes to the same file by multiple Nix processes are not supported and + may result in interleaved or corrupted log records. + )"}; }; extern LoggerSettings loggerSettings; @@ -74,6 +88,17 @@ public: virtual void stop() { }; + /** + * Guard object to resume the logger when done. + */ + struct Suspension { + Finally> _finalize; + }; + + Suspension suspend(); + + std::optional suspendIf(bool cond); + virtual void pause() { }; virtual void resume() { }; @@ -179,20 +204,44 @@ struct PushActivity ~PushActivity() { setCurActivity(prevAct); } }; -extern Logger * logger; +extern std::unique_ptr logger; -Logger * makeSimpleLogger(bool printBuildLogs = true); +std::unique_ptr makeSimpleLogger(bool printBuildLogs = true); -Logger * makeJSONLogger(Logger & prevLogger); +/** + * Create a logger that sends log messages to `mainLogger` and the + * list of loggers in `extraLoggers`. Only `mainLogger` is used for + * writing to stdout and getting user input. + */ +std::unique_ptr makeTeeLogger( + std::unique_ptr mainLogger, + std::vector> && extraLoggers); -std::optional parseJSONMessage(const std::string & msg); +std::unique_ptr makeJSONLogger(Descriptor fd, bool includeNixPrefix = true); +std::unique_ptr makeJSONLogger(const std::filesystem::path & path, bool includeNixPrefix = true); + +void applyJSONLogger(); + +/** + * @param source A noun phrase describing the source of the message, e.g. "the builder". + */ +std::optional parseJSONMessage(const std::string & msg, std::string_view source); + +/** + * @param source A noun phrase describing the source of the message, e.g. "the builder". + */ bool handleJSONLogMessage(nlohmann::json & json, const Activity & act, std::map & activities, + std::string_view source, bool trusted); +/** + * @param source A noun phrase describing the source of the message, e.g. "the builder". + */ bool handleJSONLogMessage(const std::string & msg, const Activity & act, std::map & activities, + std::string_view source, bool trusted); /** diff --git a/src/libutil/include/nix/util/lru-cache.hh b/src/libutil/include/nix/util/lru-cache.hh new file mode 100644 index 00000000000..0834a8e7496 --- /dev/null +++ b/src/libutil/include/nix/util/lru-cache.hh @@ -0,0 +1,144 @@ +#pragma once +///@file + +#include +#include +#include +#include + +namespace nix { + +/** + * A simple least-recently used cache. Not thread-safe. + */ +template> +class LRUCache +{ +private: + + size_t capacity; + + // Stupid wrapper to get around circular dependency between Data + // and LRU. + struct LRUIterator; + + using Data = std::map, Compare>; + using LRU = std::list; + + struct LRUIterator + { + typename LRU::iterator it; + }; + + Data data; + LRU lru; + + /** + * Move this item to the back of the LRU list. + */ + void promote(LRU::iterator it) + { + /* Think of std::list iterators as stable pointers to the list node, + * which never get invalidated. Thus, we can reuse the same lru list + * element and just splice it to the back of the list without the need + * to update its value in the key -> list iterator map. */ + lru.splice(/*pos=*/lru.end(), /*other=*/lru, it); + } + +public: + + LRUCache(size_t capacity) + : capacity(capacity) + { + } + + /** + * Insert or upsert an item in the cache. + */ + template + void upsert(const K & key, const Value & value) + { + if (capacity == 0) + return; + + erase(key); + + if (data.size() >= capacity) { + /** + * Retire the oldest item. + */ + auto oldest = lru.begin(); + data.erase(*oldest); + lru.erase(oldest); + } + + auto res = data.emplace(key, std::make_pair(LRUIterator(), value)); + assert(res.second); + auto & i(res.first); + + auto j = lru.insert(lru.end(), i); + + i->second.first.it = j; + } + + template + bool erase(const K & key) + { + auto i = data.find(key); + if (i == data.end()) + return false; + lru.erase(i->second.first.it); + data.erase(i); + return true; + } + + /** + * Look up an item in the cache. If it exists, it becomes the most + * recently used item. + * + * @returns corresponding cache entry, std::nullopt if it's not in the cache + */ + template + std::optional get(const K & key) + { + auto i = data.find(key); + if (i == data.end()) + return {}; + + auto & [it, value] = i->second; + promote(it.it); + return value; + } + + /** + * Look up an item in the cache. If it exists, it becomes the most + * recently used item. + * + * @returns mutable pointer to the corresponding cache entry, nullptr if + * it's not in the cache + */ + template + Value * getOrNullptr(const K & key) + { + auto i = data.find(key); + if (i == data.end()) + return nullptr; + + auto & [it, value] = i->second; + promote(it.it); + return &value; + } + + size_t size() const noexcept + { + return data.size(); + } + + void clear() noexcept + { + data.clear(); + lru.clear(); + } +}; + +} diff --git a/src/libutil/memory-source-accessor.hh b/src/libutil/include/nix/util/memory-source-accessor.hh similarity index 97% rename from src/libutil/memory-source-accessor.hh rename to src/libutil/include/nix/util/memory-source-accessor.hh index 012a388c0e7..d09ba153d70 100644 --- a/src/libutil/memory-source-accessor.hh +++ b/src/libutil/include/nix/util/memory-source-accessor.hh @@ -1,6 +1,6 @@ -#include "source-path.hh" -#include "fs-sink.hh" -#include "variant-wrapper.hh" +#include "nix/util/source-path.hh" +#include "nix/util/fs-sink.hh" +#include "nix/util/variant-wrapper.hh" namespace nix { diff --git a/src/libutil/include/nix/util/meson.build b/src/libutil/include/nix/util/meson.build new file mode 100644 index 00000000000..22438c1d075 --- /dev/null +++ b/src/libutil/include/nix/util/meson.build @@ -0,0 +1,83 @@ +# Public headers directory + +include_dirs = [ include_directories('../..') ] + +headers = files( + 'abstract-setting-to-json.hh', + 'ansicolor.hh', + 'archive.hh', + 'args.hh', + 'args/root.hh', + 'callback.hh', + 'canon-path.hh', + 'checked-arithmetic.hh', + 'chunked-vector.hh', + 'closure.hh', + 'comparator.hh', + 'compression.hh', + 'compute-levels.hh', + 'config-global.hh', + 'config-impl.hh', + 'configuration.hh', + 'current-process.hh', + 'english.hh', + 'environment-variables.hh', + 'error.hh', + 'exec.hh', + 'executable-path.hh', + 'exit.hh', + 'experimental-features.hh', + 'file-content-address.hh', + 'file-descriptor.hh', + 'file-path-impl.hh', + 'file-path.hh', + 'file-system.hh', + 'finally.hh', + 'fmt.hh', + 'fs-sink.hh', + 'git.hh', + 'hash.hh', + 'hilite.hh', + 'json-impls.hh', + 'json-utils.hh', + 'logging.hh', + 'lru-cache.hh', + 'memory-source-accessor.hh', + 'muxable-pipe.hh', + 'os-string.hh', + 'pool.hh', + 'pos-idx.hh', + 'pos-table.hh', + 'position.hh', + 'posix-source-accessor.hh', + 'processes.hh', + 'ref.hh', + 'references.hh', + 'regex-combinators.hh', + 'repair-flag.hh', + 'serialise.hh', + 'signals.hh', + 'signature/local-keys.hh', + 'signature/signer.hh', + 'sort.hh', + 'source-accessor.hh', + 'source-path.hh', + 'split.hh', + 'std-hash.hh', + 'strings-inline.hh', + 'strings.hh', + 'suggestions.hh', + 'sync.hh', + 'tarfile.hh', + 'terminal.hh', + 'thread-pool.hh', + 'topo-sort.hh', + 'types.hh', + 'unix-domain-socket.hh', + 'url-parts.hh', + 'url.hh', + 'users.hh', + 'util.hh', + 'variant-wrapper.hh', + 'xml-writer.hh', +) diff --git a/src/libutil/muxable-pipe.hh b/src/libutil/include/nix/util/muxable-pipe.hh similarity index 93% rename from src/libutil/muxable-pipe.hh rename to src/libutil/include/nix/util/muxable-pipe.hh index 53ac39170f1..d912627fbcf 100644 --- a/src/libutil/muxable-pipe.hh +++ b/src/libutil/include/nix/util/muxable-pipe.hh @@ -1,16 +1,16 @@ #pragma once ///@file -#include "file-descriptor.hh" +#include "nix/util/file-descriptor.hh" #ifdef _WIN32 -# include "windows-async-pipe.hh" +# include "nix/util/windows-async-pipe.hh" #endif #ifndef _WIN32 # include #else # include -# include "windows-error.hh" +# include "nix/util/windows-error.hh" #endif namespace nix { diff --git a/src/libutil/os-string.hh b/src/libutil/include/nix/util/os-string.hh similarity index 100% rename from src/libutil/os-string.hh rename to src/libutil/include/nix/util/os-string.hh diff --git a/src/libutil/pool.hh b/src/libutil/include/nix/util/pool.hh similarity index 98% rename from src/libutil/pool.hh rename to src/libutil/include/nix/util/pool.hh index b2ceb714342..a63db50deb5 100644 --- a/src/libutil/pool.hh +++ b/src/libutil/include/nix/util/pool.hh @@ -7,8 +7,8 @@ #include #include -#include "sync.hh" -#include "ref.hh" +#include "nix/util/sync.hh" +#include "nix/util/ref.hh" namespace nix { diff --git a/src/libexpr/pos-idx.hh b/src/libutil/include/nix/util/pos-idx.hh similarity index 94% rename from src/libexpr/pos-idx.hh rename to src/libutil/include/nix/util/pos-idx.hh index 2faa6b7fe4f..0bf59301aff 100644 --- a/src/libexpr/pos-idx.hh +++ b/src/libutil/include/nix/util/pos-idx.hh @@ -1,4 +1,5 @@ #pragma once +///@file #include #include @@ -7,7 +8,7 @@ namespace nix { class PosIdx { - friend struct LazyPosAcessors; + friend struct LazyPosAccessors; friend class PosTable; friend class std::hash; diff --git a/src/libexpr/pos-table.hh b/src/libutil/include/nix/util/pos-table.hh similarity index 58% rename from src/libexpr/pos-table.hh rename to src/libutil/include/nix/util/pos-table.hh index ba2b91cf35e..f64466c2124 100644 --- a/src/libexpr/pos-table.hh +++ b/src/libutil/include/nix/util/pos-table.hh @@ -1,11 +1,13 @@ #pragma once +///@file #include #include -#include "pos-idx.hh" -#include "position.hh" -#include "sync.hh" +#include "nix/util/lru-cache.hh" +#include "nix/util/pos-idx.hh" +#include "nix/util/position.hh" +#include "nix/util/sync.hh" namespace nix { @@ -18,9 +20,12 @@ public: private: uint32_t offset; - Origin(Pos::Origin origin, uint32_t offset, size_t size): - offset(offset), origin(origin), size(size) - {} + Origin(Pos::Origin origin, uint32_t offset, size_t size) + : offset(offset) + , origin(origin) + , size(size) + { + } public: const Pos::Origin origin; @@ -33,10 +38,20 @@ public: }; private: + /** + * Vector of byte offsets (in the virtual input buffer) of initial line character's position. + * Sorted by construction. Binary search over it allows for efficient translation of arbitrary + * byte offsets in the virtual input buffer to its line + column position. + */ using Lines = std::vector; + /** + * Cache from byte offset in the virtual buffer of Origins -> @ref Lines in that origin. + */ + using LinesCache = LRUCache; std::map origins; - mutable Sync> lines; + + mutable Sync linesCache; const Origin * resolve(PosIdx p) const { @@ -52,6 +67,11 @@ private: } public: + PosTable(std::size_t linesCacheCapacity = 65536) + : linesCache(linesCacheCapacity) + { + } + Origin addOrigin(Pos::Origin origin, size_t size) { uint32_t offset = 0; @@ -72,6 +92,17 @@ public: return PosIdx(1 + origin.offset + offset); } + /** + * Convert a byte-offset PosIdx into a Pos with line/column information. + * + * @param p Byte offset into the virtual concatenation of all parsed contents + * @return Position + * + * @warning Very expensive to call, as this has to read the entire source + * into memory each time. Call this only if absolutely necessary. Prefer + * to keep PosIdx around instead of needlessly converting it into Pos by + * using this lookup method. + */ Pos operator[](PosIdx p) const; Pos::Origin originOf(PosIdx p) const diff --git a/src/libutil/position.hh b/src/libutil/include/nix/util/position.hh similarity index 90% rename from src/libutil/position.hh rename to src/libutil/include/nix/util/position.hh index 25217069c14..34cf86392c1 100644 --- a/src/libutil/position.hh +++ b/src/libutil/include/nix/util/position.hh @@ -9,7 +9,7 @@ #include #include -#include "source-path.hh" +#include "nix/util/source-path.hh" namespace nix { @@ -43,14 +43,10 @@ struct Pos Pos() { } Pos(uint32_t line, uint32_t column, Origin origin) : line(line), column(column), origin(origin) { } - Pos(Pos & other) = default; - Pos(const Pos & other) = default; - Pos(Pos && other) = default; - Pos(const Pos * other); explicit operator bool() const { return line > 0; } - operator std::shared_ptr() const; + operator std::shared_ptr() const; /** * Return the contents of the source file. @@ -69,9 +65,7 @@ struct Pos /** * Get the SourcePath, if the source was loaded from a file. */ - std::optional getSourcePath() const { - return *std::get_if(&origin); - } + std::optional getSourcePath() const; struct LinesIterator { using difference_type = size_t; diff --git a/src/libutil/posix-source-accessor.hh b/src/libutil/include/nix/util/posix-source-accessor.hh similarity index 68% rename from src/libutil/posix-source-accessor.hh rename to src/libutil/include/nix/util/posix-source-accessor.hh index 40f60bb54b8..ea65b148f7d 100644 --- a/src/libutil/posix-source-accessor.hh +++ b/src/libutil/include/nix/util/posix-source-accessor.hh @@ -1,6 +1,6 @@ #pragma once -#include "source-accessor.hh" +#include "nix/util/source-accessor.hh" namespace nix { @@ -43,13 +43,25 @@ struct PosixSourceAccessor : virtual SourceAccessor std::optional getPhysicalPath(const CanonPath & path) override; /** - * Create a `PosixSourceAccessor` and `CanonPath` corresponding to + * Create a `PosixSourceAccessor` and `SourcePath` corresponding to * some native path. * * The `PosixSourceAccessor` is rooted as far up the tree as * possible, (e.g. on Windows it could scoped to a drive like * `C:\`). This allows more `..` parent accessing to work. * + * @note When `path` is trusted user input, canonicalize it using + * `std::filesystem::canonical`, `makeParentCanonical`, `std::filesystem::weakly_canonical`, etc, + * as appropriate for the use case. At least weak canonicalization is + * required for the `SourcePath` to do anything useful at the location it + * points to. + * + * @note A canonicalizing behavior is not built in `createAtRoot` so that + * callers do not accidentally introduce symlink-related security vulnerabilities. + * Furthermore, `createAtRoot` does not know whether the file pointed to by + * `path` should be resolved if it is itself a symlink. In other words, + * `createAtRoot` can not decide between aforementioned `canonical`, `makeParentCanonical`, etc. for its callers. + * * See * [`std::filesystem::path::root_path`](https://en.cppreference.com/w/cpp/filesystem/path/root_path) * and diff --git a/src/libutil/processes.hh b/src/libutil/include/nix/util/processes.hh similarity index 92% rename from src/libutil/processes.hh rename to src/libutil/include/nix/util/processes.hh index bbbe7dcabd3..ab5f23e49ad 100644 --- a/src/libutil/processes.hh +++ b/src/libutil/include/nix/util/processes.hh @@ -1,11 +1,11 @@ #pragma once ///@file -#include "types.hh" -#include "error.hh" -#include "file-descriptor.hh" -#include "logging.hh" -#include "ansicolor.hh" +#include "nix/util/types.hh" +#include "nix/util/error.hh" +#include "nix/util/file-descriptor.hh" +#include "nix/util/logging.hh" +#include "nix/util/ansicolor.hh" #include #include @@ -103,7 +103,7 @@ struct RunOptions std::optional gid; #endif std::optional chdir; - std::optional> environment; + std::optional environment; std::optional input; Source * standardIn = nullptr; Sink * standardOut = nullptr; diff --git a/src/libutil/ref.hh b/src/libutil/include/nix/util/ref.hh similarity index 100% rename from src/libutil/ref.hh rename to src/libutil/include/nix/util/ref.hh diff --git a/src/libutil/references.hh b/src/libutil/include/nix/util/references.hh similarity index 97% rename from src/libutil/references.hh rename to src/libutil/include/nix/util/references.hh index 8bc9f7ec9d6..89a42e00948 100644 --- a/src/libutil/references.hh +++ b/src/libutil/include/nix/util/references.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "hash.hh" +#include "nix/util/hash.hh" namespace nix { diff --git a/src/libutil/regex-combinators.hh b/src/libutil/include/nix/util/regex-combinators.hh similarity index 60% rename from src/libutil/regex-combinators.hh rename to src/libutil/include/nix/util/regex-combinators.hh index 87d6aa6785f..75ccd4e6cf0 100644 --- a/src/libutil/regex-combinators.hh +++ b/src/libutil/include/nix/util/regex-combinators.hh @@ -2,6 +2,8 @@ ///@file #include +#include +#include namespace nix::regex { @@ -10,22 +12,23 @@ namespace nix::regex { static inline std::string either(std::string_view a, std::string_view b) { - return std::string { a } + "|" + b; + std::stringstream ss; + ss << a << "|" << b; + return ss.str(); } static inline std::string group(std::string_view a) { - return std::string { "(" } + a + ")"; -} - -static inline std::string many(std::string_view a) -{ - return std::string { "(?:" } + a + ")*"; + std::stringstream ss; + ss << "(" << a << ")"; + return ss.str(); } static inline std::string list(std::string_view a) { - return std::string { a } + many(group("," + a)); + std::stringstream ss; + ss << a << "(," << a << ")*"; + return ss.str(); } } diff --git a/src/libutil/repair-flag.hh b/src/libutil/include/nix/util/repair-flag.hh similarity index 100% rename from src/libutil/repair-flag.hh rename to src/libutil/include/nix/util/repair-flag.hh diff --git a/src/libutil/serialise.hh b/src/libutil/include/nix/util/serialise.hh similarity index 98% rename from src/libutil/serialise.hh rename to src/libutil/include/nix/util/serialise.hh index 14721d0693e..97fdddae301 100644 --- a/src/libutil/serialise.hh +++ b/src/libutil/include/nix/util/serialise.hh @@ -4,9 +4,9 @@ #include #include -#include "types.hh" -#include "util.hh" -#include "file-descriptor.hh" +#include "nix/util/types.hh" +#include "nix/util/util.hh" +#include "nix/util/file-descriptor.hh" namespace boost::context { struct stack_context; } @@ -564,7 +564,7 @@ struct FramedSink : nix::BufferedSink void writeUnbuffered(std::string_view data) override { - /* Don't send more data if an error has occured. */ + /* Don't send more data if an error has occurred. */ checkError(); to << data.size(); diff --git a/src/libutil/signals.hh b/src/libutil/include/nix/util/signals.hh similarity index 83% rename from src/libutil/signals.hh rename to src/libutil/include/nix/util/signals.hh index 8bff345c357..5a2ba8e75b7 100644 --- a/src/libutil/signals.hh +++ b/src/libutil/include/nix/util/signals.hh @@ -1,9 +1,9 @@ #pragma once ///@file -#include "types.hh" -#include "error.hh" -#include "logging.hh" +#include "nix/util/types.hh" +#include "nix/util/error.hh" +#include "nix/util/logging.hh" #include @@ -26,6 +26,11 @@ static inline bool getInterrupted(); */ void setInterruptThrown(); +/** + * @note Does nothing on Windows + */ +static inline bool isInterrupted(); + /** * @note Does nothing on Windows */ @@ -62,4 +67,4 @@ struct ReceiveInterrupts; } -#include "signals-impl.hh" +#include "nix/util/signals-impl.hh" diff --git a/src/libutil/signature/local-keys.hh b/src/libutil/include/nix/util/signature/local-keys.hh similarity index 98% rename from src/libutil/signature/local-keys.hh rename to src/libutil/include/nix/util/signature/local-keys.hh index 9977f0dac6e..85918f90602 100644 --- a/src/libutil/signature/local-keys.hh +++ b/src/libutil/include/nix/util/signature/local-keys.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "types.hh" +#include "nix/util/types.hh" #include diff --git a/src/libutil/signature/signer.hh b/src/libutil/include/nix/util/signature/signer.hh similarity index 94% rename from src/libutil/signature/signer.hh rename to src/libutil/include/nix/util/signature/signer.hh index e50170fe29c..ca2905eefcd 100644 --- a/src/libutil/signature/signer.hh +++ b/src/libutil/include/nix/util/signature/signer.hh @@ -1,7 +1,7 @@ #pragma once -#include "types.hh" -#include "signature/local-keys.hh" +#include "nix/util/types.hh" +#include "nix/util/signature/local-keys.hh" #include #include diff --git a/src/libutil/include/nix/util/sort.hh b/src/libutil/include/nix/util/sort.hh new file mode 100644 index 00000000000..0affdf3ce97 --- /dev/null +++ b/src/libutil/include/nix/util/sort.hh @@ -0,0 +1,299 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +/** + * @file + * + * In-house implementation of sorting algorithms. Used for cases when several properties + * need to be upheld regardless of the stdlib implementation of std::sort or + * std::stable_sort. + * + * PeekSort implementation is adapted from reference implementation + * https://github.com/sebawild/powersort licensed under the MIT License. + * + */ + +/* PeekSort attribution: + * + * MIT License + * + * Copyright (c) 2022 Sebastian Wild + * + * 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: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * 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. + */ + +namespace nix { + +/** + * Merge sorted runs [begin, middle) with [middle, end) in-place [begin, end). + * Uses a temporary working buffer by first copying [begin, end) to it. + * + * @param begin Start of the first subrange to be sorted. + * @param middle End of the first sorted subrange and the start of the second. + * @param end End of the second sorted subrange. + * @param workingBegin Start of the working buffer. + * @param comp Comparator implementing an operator()(const ValueType& lhs, const ValueType& rhs). + * + * @pre workingBegin buffer must have at least std::distance(begin, end) elements. + * + * @note We can't use std::inplace_merge or std::merge, because their behavior + * is undefined if the comparator is not strict weak ordering. + */ +template< + std::forward_iterator Iter, + std::random_access_iterator BufIter, + typename Comparator = std::less>> +void mergeSortedRunsInPlace(Iter begin, Iter middle, Iter end, BufIter workingBegin, Comparator comp = {}) +{ + const BufIter workingMiddle = std::move(begin, middle, workingBegin); + const BufIter workingEnd = std::move(middle, end, workingMiddle); + + Iter output = begin; + BufIter workingLeft = workingBegin; + BufIter workingRight = workingMiddle; + + while (workingLeft != workingMiddle && workingRight != workingEnd) { + /* Note the inversion here !comp(...., ....). This is required for the merge to be stable. + If a == b where a if from the left part and b is the the right, then we have to pick + a. */ + *output++ = !comp(*workingRight, *workingLeft) ? std::move(*workingLeft++) : std::move(*workingRight++); + } + + std::move(workingLeft, workingMiddle, output); + std::move(workingRight, workingEnd, output); +} + +/** + * Simple insertion sort. + * + * Does not require that the std::iter_value_t is copyable. + * + * @param begin Start of the range to sort. + * @param end End of the range to sort. + * @comp Comparator the defines the ordering. Order of elements if the comp is not strict weak ordering + * is not specified. + * @throws Nothing. + * + * Note on exception safety: this function provides weak exception safety + * guarantees. To elaborate: if the comparator throws or move assignment + * throws (value type is not nothrow_move_assignable) then the range is left in + * a consistent, but unspecified state. + * + * @note This can't be implemented in terms of binary search if the strict weak ordering + * needs to be handled in a well-defined but unspecified manner. + */ +template>> +void insertionsort(Iter begin, Iter end, Comparator comp = {}) +{ + if (begin == end) + return; + for (Iter current = std::next(begin); current != end; ++current) { + for (Iter insertionPoint = current; + insertionPoint != begin && comp(*insertionPoint, *std::prev(insertionPoint)); + --insertionPoint) { + std::swap(*insertionPoint, *std::prev(insertionPoint)); + } + } +} + +/** + * Find maximal i <= end such that [begin, i) is strictly decreasing according + * to the specified comparator. + */ +template>> +Iter strictlyDecreasingPrefix(Iter begin, Iter end, Comparator && comp = {}) +{ + if (begin == end) + return begin; + while (std::next(begin) != end && /* *std::next(begin) < begin */ + comp(*std::next(begin), *begin)) + ++begin; + return std::next(begin); +} + +/** + * Find minimal i >= start such that [i, end) is strictly decreasing according + * to the specified comparator. + */ +template>> +Iter strictlyDecreasingSuffix(Iter begin, Iter end, Comparator && comp = {}) +{ + if (begin == end) + return end; + while (std::prev(end) > begin && /* *std::prev(end) < *std::prev(end, 2) */ + comp(*std::prev(end), *std::prev(end, 2))) + --end; + return std::prev(end); +} + +/** + * Find maximal i <= end such that [begin, i) is weakly increasing according + * to the specified comparator. + */ +template>> +Iter weaklyIncreasingPrefix(Iter begin, Iter end, Comparator && comp = {}) +{ + return strictlyDecreasingPrefix(begin, end, std::not_fn(std::forward(comp))); +} + +/** + * Find minimal i >= start such that [i, end) is weakly increasing according + * to the specified comparator. + */ +template>> +Iter weaklyIncreasingSuffix(Iter begin, Iter end, Comparator && comp = {}) +{ + return strictlyDecreasingSuffix(begin, end, std::not_fn(std::forward(comp))); +} + +/** + * Peeksort stable sorting algorithm. Sorts elements in-place. + * Allocates additional memory as needed. + * + * @details + * PeekSort is a stable, near-optimal natural mergesort. Most importantly, like any + * other mergesort it upholds the "Ord safety" property. Meaning that even for + * comparator predicates that don't satisfy strict weak ordering it can't result + * in infinite loops/out of bounds memory accesses or other undefined behavior. + * + * As a quick reminder, strict weak ordering relation operator< must satisfy + * the following properties. Keep in mind that in C++ an equvalence relation + * is specified in terms of operator< like so: a ~ b iff !(a < b) && !(b < a). + * + * 1. a < a === false - relation is irreflexive + * 2. a < b, b < c => a < c - transitivity + * 3. a ~ b, a ~ b, b ~ c => a ~ c, transitivity of equivalence + * + * @see https://www.wild-inter.net/publications/munro-wild-2018 + * @see https://github.com/Voultapher/sort-research-rs/blob/main/writeup/sort_safety/text.md#property-analysis + * + * The order of elements when comp is not strict weak ordering is not specified, but + * is not undefined. The output is always some permutation of the input, regardless + * of the comparator provided. + * Relying on ordering in such cases is erroneous, but this implementation + * will happily accept broken comparators and will not crash. + * + * @param begin Start of the range to be sorted. + * @param end End of the range to be sorted. + * @comp comp Comparator implementing an operator()(const ValueType& lhs, const ValueType& rhs). + * + * @throws std::bad_alloc if the temporary buffer can't be allocated. + * + * @return Nothing. + * + * Note on exception safety: this function provides weak exception safety + * guarantees. To elaborate: if the comparator throws or move assignment + * throws (value type is not nothrow_move_assignable) then the range is left in + * a consistent, but unspecified state. + * + */ +template>> +/* ValueType must be default constructible to create the temporary buffer */ + requires std::is_default_constructible_v> +void peeksort(Iter begin, Iter end, Comparator comp = {}) +{ + auto length = std::distance(begin, end); + + /* Special-case very simple inputs. This is identical to how libc++ does it. */ + switch (length) { + case 0: + [[fallthrough]]; + case 1: + return; + case 2: + if (comp(*--end, *begin)) /* [a, b], b < a */ + std::swap(*begin, *end); + return; + } + + using ValueType = std::iter_value_t; + auto workingBuffer = std::vector(length); + + /* + * sorts [begin, end), assuming that [begin, leftRunEnd) and + * [rightRunBegin, end) are sorted. + * Modified implementation from: + * https://github.com/sebawild/powersort/blob/1d078b6be9023e134c4f8f6de88e2406dc681e89/src/sorts/peeksort.h + */ + auto peeksortImpl = [&workingBuffer, + &comp](auto & peeksortImpl, Iter begin, Iter end, Iter leftRunEnd, Iter rightRunBegin) { + if (leftRunEnd == end || rightRunBegin == begin) + return; + + /* Dispatch to simpler insertion sort implementation for smaller cases + Cut-off limit is the same as in libstdc++ + https://github.com/gcc-mirror/gcc/blob/d9375e490072d1aae73a93949aa158fcd2a27018/libstdc%2B%2B-v3/include/bits/stl_algo.h#L4977 + */ + static constexpr std::size_t insertionsortThreshold = 16; + size_t length = std::distance(begin, end); + if (length <= insertionsortThreshold) + return insertionsort(begin, end, comp); + + Iter middle = std::next(begin, (length / 2)); /* Middle split between m and m - 1 */ + + if (middle <= leftRunEnd) { + /* |XXXXXXXX|XX X| */ + peeksortImpl(peeksortImpl, leftRunEnd, end, std::next(leftRunEnd), rightRunBegin); + mergeSortedRunsInPlace(begin, leftRunEnd, end, workingBuffer.begin(), comp); + return; + } else if (middle >= rightRunBegin) { + /* |XX X|XXXXXXXX| */ + peeksortImpl(peeksortImpl, begin, rightRunBegin, leftRunEnd, std::prev(rightRunBegin)); + mergeSortedRunsInPlace(begin, rightRunBegin, end, workingBuffer.begin(), comp); + return; + } + + /* Find middle run, i.e., run containing m - 1 */ + Iter i, j; + + if (!comp(*middle, *std::prev(middle)) /* *std::prev(middle) <= *middle */) { + i = weaklyIncreasingSuffix(leftRunEnd, middle, comp); + j = weaklyIncreasingPrefix(std::prev(middle), rightRunBegin, comp); + } else { + i = strictlyDecreasingSuffix(leftRunEnd, middle, comp); + j = strictlyDecreasingPrefix(std::prev(middle), rightRunBegin, comp); + std::reverse(i, j); + } + + if (i == begin && j == end) + return; /* single run */ + + if (middle - i < j - middle) { + /* |XX x|xxxx X| */ + peeksortImpl(peeksortImpl, begin, i, leftRunEnd, std::prev(i)); + peeksortImpl(peeksortImpl, i, end, j, rightRunBegin); + mergeSortedRunsInPlace(begin, i, end, workingBuffer.begin(), comp); + } else { + /* |XX xxx|x X| */ + peeksortImpl(peeksortImpl, begin, j, leftRunEnd, i); + peeksortImpl(peeksortImpl, j, end, std::next(j), rightRunBegin); + mergeSortedRunsInPlace(begin, j, end, workingBuffer.begin(), comp); + } + }; + + peeksortImpl(peeksortImpl, begin, end, /*leftRunEnd=*/begin, /*rightRunBegin=*/end); +} + +} diff --git a/src/libutil/source-accessor.hh b/src/libutil/include/nix/util/source-accessor.hh similarity index 87% rename from src/libutil/source-accessor.hh rename to src/libutil/include/nix/util/source-accessor.hh index b16960d4a4c..92a9adc46e9 100644 --- a/src/libutil/source-accessor.hh +++ b/src/libutil/include/nix/util/source-accessor.hh @@ -2,9 +2,9 @@ #include -#include "canon-path.hh" -#include "hash.hh" -#include "ref.hh" +#include "nix/util/canon-path.hh" +#include "nix/util/hash.hh" +#include "nix/util/ref.hh" namespace nix { @@ -54,7 +54,7 @@ struct SourceAccessor : std::enable_shared_from_this * * @note Unlike Unix, this method should *not* follow symlinks. Nix * by default wants to manipulate symlinks explicitly, and not - * implictly follow them, as they are frequently untrusted user data + * implicitly follow them, as they are frequently untrusted user data * and thus may point to arbitrary locations. Acting on the targets * targets of symlinks should only occasionally be done, and only * with care. @@ -88,12 +88,13 @@ struct SourceAccessor : std::enable_shared_from_this Unlike `DT_UNKNOWN`, this must not be used for deferring the lookup of types. */ - tMisc + tChar, tBlock, tSocket, tFifo, + tUnknown }; struct Stat { - Type type = tMisc; + Type type = tUnknown; /** * For regular files only: the size of the file. Not all @@ -112,6 +113,9 @@ struct SourceAccessor : std::enable_shared_from_this * file in the NAR. Only returned by NAR accessors. */ std::optional narOffset; + + bool isNotNARSerialisable(); + std::string typeString(); }; Stat lstat(const CanonPath & path); @@ -210,4 +214,18 @@ ref getFSSourceAccessor(); */ ref makeFSSourceAccessor(std::filesystem::path root); +ref makeMountedSourceAccessor(std::map> mounts); + +/** + * Construct an accessor that presents a "union" view of a vector of + * underlying accessors. Earlier accessors take precedence over later. + */ +ref makeUnionSourceAccessor(std::vector> && accessors); + +/** + * Creates a new source accessor which is confined to the subdirectory + * of the given source accessor. + */ +ref projectSubdirSourceAccessor(ref, CanonPath subdirectory); + } diff --git a/src/libutil/source-path.hh b/src/libutil/include/nix/util/source-path.hh similarity index 96% rename from src/libutil/source-path.hh rename to src/libutil/include/nix/util/source-path.hh index fc2288f747a..c0cba024103 100644 --- a/src/libutil/source-path.hh +++ b/src/libutil/include/nix/util/source-path.hh @@ -5,10 +5,10 @@ * @brief SourcePath */ -#include "ref.hh" -#include "canon-path.hh" -#include "source-accessor.hh" -#include "std-hash.hh" +#include "nix/util/ref.hh" +#include "nix/util/canon-path.hh" +#include "nix/util/source-accessor.hh" +#include "nix/util/std-hash.hh" namespace nix { diff --git a/src/libutil/split.hh b/src/libutil/include/nix/util/split.hh similarity index 97% rename from src/libutil/split.hh rename to src/libutil/include/nix/util/split.hh index 3b9b2b83b81..24a73fea85f 100644 --- a/src/libutil/split.hh +++ b/src/libutil/include/nix/util/split.hh @@ -4,7 +4,7 @@ #include #include -#include "util.hh" +#include "nix/util/util.hh" namespace nix { diff --git a/src/libutil/std-hash.hh b/src/libutil/include/nix/util/std-hash.hh similarity index 100% rename from src/libutil/std-hash.hh rename to src/libutil/include/nix/util/std-hash.hh diff --git a/src/libutil/strings-inline.hh b/src/libutil/include/nix/util/strings-inline.hh similarity index 98% rename from src/libutil/strings-inline.hh rename to src/libutil/include/nix/util/strings-inline.hh index 25b8e0ff67e..d99b686fc13 100644 --- a/src/libutil/strings-inline.hh +++ b/src/libutil/include/nix/util/strings-inline.hh @@ -1,6 +1,6 @@ #pragma once -#include "strings.hh" +#include "nix/util/strings.hh" namespace nix { diff --git a/src/libutil/strings.hh b/src/libutil/include/nix/util/strings.hh similarity index 56% rename from src/libutil/strings.hh rename to src/libutil/include/nix/util/strings.hh index 533126be1e0..4c77516a30b 100644 --- a/src/libutil/strings.hh +++ b/src/libutil/include/nix/util/strings.hh @@ -1,11 +1,15 @@ #pragma once +#include "nix/util/types.hh" + #include #include #include #include #include +#include + namespace nix { /* @@ -28,7 +32,7 @@ template C tokenizeString(std::string_view s, std::string_view separators = " \t\n\r"); extern template std::list tokenizeString(std::string_view s, std::string_view separators); -extern template std::set tokenizeString(std::string_view s, std::string_view separators); +extern template StringSet tokenizeString(std::string_view s, std::string_view separators); extern template std::vector tokenizeString(std::string_view s, std::string_view separators); /** @@ -42,7 +46,7 @@ template C splitString(std::string_view s, std::string_view separators); extern template std::list splitString(std::string_view s, std::string_view separators); -extern template std::set splitString(std::string_view s, std::string_view separators); +extern template StringSet splitString(std::string_view s, std::string_view separators); extern template std::vector splitString(std::string_view s, std::string_view separators); /** @@ -52,8 +56,23 @@ template std::string concatStringsSep(const std::string_view sep, const C & ss); extern template std::string concatStringsSep(std::string_view, const std::list &); -extern template std::string concatStringsSep(std::string_view, const std::set &); +extern template std::string concatStringsSep(std::string_view, const StringSet &); extern template std::string concatStringsSep(std::string_view, const std::vector &); +extern template std::string concatStringsSep(std::string_view, const boost::container::small_vector &); + +/** + * Apply a function to the `iterable`'s items and concat them with `separator`. + */ +template +std::string concatMapStringsSep(std::string_view separator, const C & iterable, F fn) +{ + boost::container::small_vector strings; + strings.reserve(iterable.size()); + for (const auto & elem : iterable) { + strings.push_back(fn(elem)); + } + return concatStringsSep(separator, strings); +} /** * Ignore any empty strings at the start of the list, and then concatenate the @@ -68,7 +87,49 @@ template dropEmptyInitThenConcatStringsSep(const std::string_view sep, const C & ss); extern template std::string dropEmptyInitThenConcatStringsSep(std::string_view, const std::list &); -extern template std::string dropEmptyInitThenConcatStringsSep(std::string_view, const std::set &); +extern template std::string dropEmptyInitThenConcatStringsSep(std::string_view, const StringSet &); extern template std::string dropEmptyInitThenConcatStringsSep(std::string_view, const std::vector &); +/** + * Shell split string: split a string into shell arguments, respecting quotes and backslashes. + * + * Used for NIX_SSHOPTS handling, which previously used `tokenizeString` and was broken by + * Arguments that need to be passed to ssh with spaces in them. + */ +std::list shellSplitString(std::string_view s); + +/** + * Hash implementation that can be used for zero-copy heterogenous lookup from + * P1690R1[1] in unordered containers. + * + * [1]: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1690r1.html + */ +struct StringViewHash +{ +private: + using HashType = std::hash; + +public: + using is_transparent = void; + + auto operator()(const char * str) const + { + /* This has a slight overhead due to an implicit strlen, but there isn't + a good way around it because the hash value of all overloads must be + consistent. Delegating to string_view is the solution initially proposed + in P0919R3. */ + return HashType{}(std::string_view{str}); + } + + auto operator()(std::string_view str) const + { + return HashType{}(str); + } + + auto operator()(const std::string & str) const + { + return HashType{}(std::string_view{str}); + } +}; + } diff --git a/src/libutil/suggestions.hh b/src/libutil/include/nix/util/suggestions.hh similarity index 96% rename from src/libutil/suggestions.hh rename to src/libutil/include/nix/util/suggestions.hh index e39ab400c0d..6a76eb9d9c1 100644 --- a/src/libutil/suggestions.hh +++ b/src/libutil/include/nix/util/suggestions.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "types.hh" +#include "nix/util/types.hh" #include namespace nix { @@ -35,7 +35,7 @@ public: ) const; static Suggestions bestMatches ( - const std::set & allMatches, + const StringSet & allMatches, std::string_view query ); diff --git a/src/libutil/sync.hh b/src/libutil/include/nix/util/sync.hh similarity index 96% rename from src/libutil/sync.hh rename to src/libutil/include/nix/util/sync.hh index d340f3d9760..4b9d546d2b7 100644 --- a/src/libutil/sync.hh +++ b/src/libutil/include/nix/util/sync.hh @@ -7,7 +7,7 @@ #include #include -#include "error.hh" +#include "nix/util/error.hh" namespace nix { @@ -39,6 +39,7 @@ public: SyncBase() { } SyncBase(const T & data) : data(data) { } SyncBase(T && data) noexcept : data(std::move(data)) { } + SyncBase(SyncBase && other) noexcept : data(std::move(*other.lock())) { } template class Lock diff --git a/src/libutil/tarfile.hh b/src/libutil/include/nix/util/tarfile.hh similarity index 95% rename from src/libutil/tarfile.hh rename to src/libutil/include/nix/util/tarfile.hh index 5e29c6bbac3..2005d13ca36 100644 --- a/src/libutil/tarfile.hh +++ b/src/libutil/include/nix/util/tarfile.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "serialise.hh" -#include "fs-sink.hh" +#include "nix/util/serialise.hh" +#include "nix/util/fs-sink.hh" #include namespace nix { diff --git a/src/libutil/terminal.hh b/src/libutil/include/nix/util/terminal.hh similarity index 100% rename from src/libutil/terminal.hh rename to src/libutil/include/nix/util/terminal.hh diff --git a/src/libutil/thread-pool.hh b/src/libutil/include/nix/util/thread-pool.hh similarity index 87% rename from src/libutil/thread-pool.hh rename to src/libutil/include/nix/util/thread-pool.hh index 02765badc82..92009e396ce 100644 --- a/src/libutil/thread-pool.hh +++ b/src/libutil/include/nix/util/thread-pool.hh @@ -1,8 +1,8 @@ #pragma once ///@file -#include "error.hh" -#include "sync.hh" +#include "nix/util/error.hh" +#include "nix/util/sync.hh" #include #include @@ -83,7 +83,6 @@ private: */ template void processGraph( - ThreadPool & pool, const std::set & nodes, std::function(const T &)> getEdges, std::function processNode) @@ -97,6 +96,10 @@ void processGraph( std::function worker; + /* Create pool last to ensure threads are stopped before other destructors + * run */ + ThreadPool pool; + worker = [&](const T & node) { { @@ -147,8 +150,16 @@ void processGraph( } }; - for (auto & node : nodes) - pool.enqueue(std::bind(worker, std::ref(node))); + for (auto & node : nodes) { + try { + pool.enqueue(std::bind(worker, std::ref(node))); + } catch (ThreadPoolShutDown &) { + /* Stop if the thread pool is shutting down. It means a + previous work item threw an exception, so process() + below will rethrow it. */ + break; + } + } pool.process(); diff --git a/src/libutil/topo-sort.hh b/src/libutil/include/nix/util/topo-sort.hh similarity index 75% rename from src/libutil/topo-sort.hh rename to src/libutil/include/nix/util/topo-sort.hh index a52811fbf41..6ba6fda713c 100644 --- a/src/libutil/topo-sort.hh +++ b/src/libutil/include/nix/util/topo-sort.hh @@ -1,17 +1,17 @@ #pragma once ///@file -#include "error.hh" +#include "nix/util/error.hh" namespace nix { -template -std::vector topoSort(std::set items, - std::function(const T &)> getChildren, +template +std::vector topoSort(std::set items, + std::function(const T &)> getChildren, std::function makeCycleError) { std::vector sorted; - std::set visited, parents; + decltype(items) visited, parents; std::function dfsVisit; @@ -21,7 +21,7 @@ std::vector topoSort(std::set items, if (!visited.insert(path).second) return; parents.insert(path); - std::set references = getChildren(path); + auto references = getChildren(path); for (auto & i : references) /* Don't traverse into items that don't exist in our starting set. */ diff --git a/src/libutil/types.hh b/src/libutil/include/nix/util/types.hh similarity index 64% rename from src/libutil/types.hh rename to src/libutil/include/nix/util/types.hh index 325e3ea7351..edb34f5e20f 100644 --- a/src/libutil/types.hh +++ b/src/libutil/include/nix/util/types.hh @@ -12,9 +12,38 @@ namespace nix { typedef std::list Strings; -typedef std::set StringSet; -typedef std::map StringMap; -typedef std::map StringPairs; + +/** + * Alias to ordered std::string -> std::string map container with transparent comparator. + * + * Used instead of std::map to use C++14 N3657 [1] + * heterogenous lookup consistently across the whole codebase. + * Transparent comparators get rid of creation of unnecessary + * temporary variables when looking up keys by `std::string_view` + * or C-style `const char *` strings. + * + * [1]: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3657.htm + */ +using StringMap = std::map>; +/** + * Alias to an ordered map of std::string -> std::string. Uses transparent comparator. + * + * @see StringMap + */ +using StringPairs = StringMap; + +/** + * Alias to ordered set container with transparent comparator. + * + * Used instead of std::set to use C++14 N3657 [1] + * heterogenous lookup consistently across the whole codebase. + * Transparent comparators get rid of creation of unnecessary + * temporary variables when looking up keys by `std::string_view` + * or C-style `const char *` strings. + * + * [1]: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3657.htm + */ +using StringSet = std::set>; /** * Paths are just strings. @@ -22,7 +51,13 @@ typedef std::map StringPairs; typedef std::string Path; typedef std::string_view PathView; typedef std::list Paths; -typedef std::set PathSet; + +/** + * Alias to an ordered set of `Path`s. Uses transparent comparator. + * + * @see StringSet + */ +using PathSet = std::set>; typedef std::vector> Headers; @@ -43,9 +78,11 @@ template struct Explicit { T t; - bool operator ==(const Explicit & other) const + bool operator ==(const Explicit & other) const = default; + + bool operator <(const Explicit & other) const { - return t == other.t; + return t < other.t; } }; diff --git a/src/libutil/unix-domain-socket.hh b/src/libutil/include/nix/util/unix-domain-socket.hh similarity index 83% rename from src/libutil/unix-domain-socket.hh rename to src/libutil/include/nix/util/unix-domain-socket.hh index ba2baeb1334..3aaaddf823d 100644 --- a/src/libutil/unix-domain-socket.hh +++ b/src/libutil/include/nix/util/unix-domain-socket.hh @@ -1,14 +1,16 @@ #pragma once ///@file -#include "types.hh" -#include "file-descriptor.hh" +#include "nix/util/types.hh" +#include "nix/util/file-descriptor.hh" #ifdef _WIN32 # include #endif #include +#include + namespace nix { /** @@ -78,6 +80,11 @@ void bind(Socket fd, const std::string & path); /** * Connect to a Unix domain socket. */ -void connect(Socket fd, const std::string & path); +void connect(Socket fd, const std::filesystem::path & path); + +/** + * Connect to a Unix domain socket. + */ +AutoCloseFD connect(const std::filesystem::path & path); } diff --git a/src/libutil/url-parts.hh b/src/libutil/include/nix/util/url-parts.hh similarity index 100% rename from src/libutil/url-parts.hh rename to src/libutil/include/nix/util/url-parts.hh diff --git a/src/libutil/url.hh b/src/libutil/include/nix/util/url.hh similarity index 84% rename from src/libutil/url.hh rename to src/libutil/include/nix/util/url.hh index 738ee9f82e6..a509f06dacf 100644 --- a/src/libutil/url.hh +++ b/src/libutil/include/nix/util/url.hh @@ -1,19 +1,16 @@ #pragma once ///@file -#include "error.hh" +#include "nix/util/error.hh" namespace nix { struct ParsedURL { - std::string url; - /// URL without query/fragment - std::string base; std::string scheme; std::optional authority; std::string path; - std::map query; + StringMap query; std::string fragment; std::string to_string() const; @@ -26,14 +23,16 @@ struct ParsedURL ParsedURL canonicalise(); }; +std::ostream & operator << (std::ostream & os, const ParsedURL & url); + MakeError(BadURL, Error); std::string percentDecode(std::string_view in); std::string percentEncode(std::string_view s, std::string_view keep=""); -std::map decodeQuery(const std::string & query); +StringMap decodeQuery(const std::string & query); -std::string encodeQuery(const std::map & query); +std::string encodeQuery(const StringMap & query); ParsedURL parseURL(const std::string & url); diff --git a/src/libutil/users.hh b/src/libutil/include/nix/util/users.hh similarity index 97% rename from src/libutil/users.hh rename to src/libutil/include/nix/util/users.hh index d22c3311d99..1d467173cd0 100644 --- a/src/libutil/users.hh +++ b/src/libutil/include/nix/util/users.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "types.hh" +#include "nix/util/types.hh" #ifndef _WIN32 # include diff --git a/src/libutil/util.hh b/src/libutil/include/nix/util/util.hh similarity index 90% rename from src/libutil/util.hh rename to src/libutil/include/nix/util/util.hh index 0fb6ff837ec..2361bf2e773 100644 --- a/src/libutil/util.hh +++ b/src/libutil/include/nix/util/util.hh @@ -1,9 +1,9 @@ #pragma once ///@file -#include "types.hh" -#include "error.hh" -#include "logging.hh" +#include "nix/util/types.hh" +#include "nix/util/error.hh" +#include "nix/util/logging.hh" #include @@ -11,7 +11,7 @@ #include #include -#include "strings.hh" +#include "nix/util/strings.hh" namespace nix { @@ -152,8 +152,13 @@ std::string toLower(std::string s); /** * Escape a string as a shell word. + * + * This always adds single quotes, even if escaping is not strictly necessary. + * So both + * - `"hello world"` -> `"'hello world'"`, which needs escaping because of the space + * - `"echo"` -> `"'echo'"`, which doesn't need escaping */ -std::string shellEscape(const std::string_view s); +std::string escapeShellArgAlways(const std::string_view s); /** @@ -274,6 +279,17 @@ std::optional pop(T & c) } +/** + * Append items to a container. TODO: remove this once we can use + * C++23's `append_range()`. + */ +template +void append(C & c, std::initializer_list l) +{ + c.insert(c.end(), l.begin(), l.end()); +} + + template class Callback; @@ -338,7 +354,9 @@ std::string showBytes(uint64_t bytes); */ inline std::string operator + (const std::string & s1, std::string_view s2) { - auto s = s1; + std::string s; + s.reserve(s1.size() + s2.size()); + s.append(s1); s.append(s2); return s; } @@ -351,10 +369,11 @@ inline std::string operator + (std::string && s, std::string_view s2) inline std::string operator + (std::string_view s1, const char * s2) { + auto s2Size = strlen(s2); std::string s; - s.reserve(s1.size() + strlen(s2)); + s.reserve(s1.size() + s2Size); s.append(s1); - s.append(s2); + s.append(s2, s2Size); return s; } diff --git a/src/libutil/variant-wrapper.hh b/src/libutil/include/nix/util/variant-wrapper.hh similarity index 100% rename from src/libutil/variant-wrapper.hh rename to src/libutil/include/nix/util/variant-wrapper.hh diff --git a/src/libutil/xml-writer.hh b/src/libutil/include/nix/util/xml-writer.hh similarity index 93% rename from src/libutil/xml-writer.hh rename to src/libutil/include/nix/util/xml-writer.hh index 74f53b7caa4..ae5a6ced7ef 100644 --- a/src/libutil/xml-writer.hh +++ b/src/libutil/include/nix/util/xml-writer.hh @@ -10,7 +10,7 @@ namespace nix { -typedef std::map XMLAttrs; +typedef std::map> XMLAttrs; class XMLWriter diff --git a/src/libutil/json-utils.cc b/src/libutil/json-utils.cc index dff068e07c5..34da83a2c86 100644 --- a/src/libutil/json-utils.cc +++ b/src/libutil/json-utils.cc @@ -1,8 +1,9 @@ -#include "json-utils.hh" -#include "error.hh" -#include "types.hh" +#include "nix/util/json-utils.hh" +#include "nix/util/error.hh" +#include "nix/util/types.hh" #include #include +#include namespace nix { @@ -38,6 +39,15 @@ std::optional optionalValueAt(const nlohmann::json::object_t & m return std::optional { map.at(key) }; } +std::optional nullableValueAt(const nlohmann::json::object_t & map, const std::string & key) +{ + auto value = valueAt(map, key); + + if (value.is_null()) + return std::nullopt; + + return std::optional { std::move(value) }; +} const nlohmann::json * getNullable(const nlohmann::json & value) { @@ -82,9 +92,18 @@ const nlohmann::json::string_t & getString(const nlohmann::json & value) return ensureType(value, nlohmann::json::value_t::string).get_ref(); } -const nlohmann::json::number_integer_t & getInteger(const nlohmann::json & value) +const nlohmann::json::number_unsigned_t & getUnsigned(const nlohmann::json & value) { - return ensureType(value, nlohmann::json::value_t::number_integer).get_ref(); + if (auto ptr = value.get()) { + return *ptr; + } + const char * typeName = value.type_name(); + if (typeName == nlohmann::json(0).type_name()) { + typeName = value.is_number_float() ? "floating point number" : "signed integral number"; + } + throw Error( + "Expected JSON value to be an unsigned integral number but it is of type '%s': %s", + typeName, value.dump()); } const nlohmann::json::boolean_t & getBoolean(const nlohmann::json & value) diff --git a/src/libutil/linux/cgroup.cc b/src/libutil/linux/cgroup.cc index ad3e8a0172f..c82fdc11cdd 100644 --- a/src/libutil/linux/cgroup.cc +++ b/src/libutil/linux/cgroup.cc @@ -1,8 +1,8 @@ -#include "cgroup.hh" -#include "signals.hh" -#include "util.hh" -#include "file-system.hh" -#include "finally.hh" +#include "nix/util/cgroup.hh" +#include "nix/util/signals.hh" +#include "nix/util/util.hh" +#include "nix/util/file-system.hh" +#include "nix/util/finally.hh" #include #include @@ -31,9 +31,9 @@ std::optional getCgroupFS() } // FIXME: obsolete, check for cgroup2 -std::map getCgroups(const Path & cgroupFile) +StringMap getCgroups(const Path & cgroupFile) { - std::map cgroups; + StringMap cgroups; for (auto & line : tokenizeString>(readFile(cgroupFile), "\n")) { static std::regex regex("([0-9]+):([^:]*):(.*)"); @@ -65,7 +65,7 @@ static CgroupStats destroyCgroup(const std::filesystem::path & cgroup, bool retu /* Otherwise, manually kill every process in the subcgroups and this cgroup. */ - for (auto & entry : std::filesystem::directory_iterator{cgroup}) { + for (auto & entry : DirectoryIterator{cgroup}) { checkInterrupt(); if (entry.symlink_status().type() != std::filesystem::file_type::directory) continue; destroyCgroup(cgroup / entry.path().filename(), false); @@ -134,7 +134,7 @@ static CgroupStats destroyCgroup(const std::filesystem::path & cgroup, bool retu } if (rmdir(cgroup.c_str()) == -1) - throw SysError("deleting cgroup '%s'", cgroup); + throw SysError("deleting cgroup %s", cgroup); return stats; } diff --git a/src/libutil/linux/cgroup.hh b/src/libutil/linux/include/nix/util/cgroup.hh similarity index 89% rename from src/libutil/linux/cgroup.hh rename to src/libutil/linux/include/nix/util/cgroup.hh index 87d135ba629..eb49c341986 100644 --- a/src/libutil/linux/cgroup.hh +++ b/src/libutil/linux/include/nix/util/cgroup.hh @@ -4,13 +4,13 @@ #include #include -#include "types.hh" +#include "nix/util/types.hh" namespace nix { std::optional getCgroupFS(); -std::map getCgroups(const Path & cgroupFile); +StringMap getCgroups(const Path & cgroupFile); struct CgroupStats { diff --git a/src/libutil/linux/namespaces.hh b/src/libutil/linux/include/nix/util/linux-namespaces.hh similarity index 95% rename from src/libutil/linux/namespaces.hh rename to src/libutil/linux/include/nix/util/linux-namespaces.hh index 208920b80b1..59db745d3d6 100644 --- a/src/libutil/linux/namespaces.hh +++ b/src/libutil/linux/include/nix/util/linux-namespaces.hh @@ -3,7 +3,7 @@ #include -#include "types.hh" +#include "nix/util/types.hh" namespace nix { diff --git a/src/libutil/linux/include/nix/util/meson.build b/src/libutil/linux/include/nix/util/meson.build new file mode 100644 index 00000000000..ec7030c4995 --- /dev/null +++ b/src/libutil/linux/include/nix/util/meson.build @@ -0,0 +1,9 @@ +# Public headers directory + +include_dirs += include_directories('../..') + +headers += files( + 'cgroup.hh', + 'linux-namespaces.hh', + # hack for trailing newline +) diff --git a/src/libutil/linux/namespaces.cc b/src/libutil/linux/linux-namespaces.cc similarity index 93% rename from src/libutil/linux/namespaces.cc rename to src/libutil/linux/linux-namespaces.cc index c5e21dffcb3..93f299076a8 100644 --- a/src/libutil/linux/namespaces.cc +++ b/src/libutil/linux/linux-namespaces.cc @@ -1,13 +1,14 @@ -#include "current-process.hh" -#include "util.hh" -#include "finally.hh" -#include "file-system.hh" -#include "processes.hh" -#include "signals.hh" +#include "nix/util/linux-namespaces.hh" +#include "nix/util/current-process.hh" +#include "nix/util/util.hh" +#include "nix/util/finally.hh" +#include "nix/util/file-system.hh" +#include "nix/util/processes.hh" +#include "nix/util/signals.hh" #include #include -#include "cgroup.hh" +#include "nix/util/cgroup.hh" #include diff --git a/src/libutil/linux/meson.build b/src/libutil/linux/meson.build index a1ded76ca16..230dd46f37c 100644 --- a/src/libutil/linux/meson.build +++ b/src/libutil/linux/meson.build @@ -1,11 +1,7 @@ sources += files( 'cgroup.cc', - 'namespaces.cc', + 'linux-namespaces.cc', + # hack for trailing newline ) -include_dirs += include_directories('.') - -headers += files( - 'cgroup.hh', - 'namespaces.hh', -) +subdir('include/nix/util') diff --git a/src/libutil/logging.cc b/src/libutil/logging.cc index 80c107ef538..4dadf15501f 100644 --- a/src/libutil/logging.cc +++ b/src/libutil/logging.cc @@ -1,11 +1,13 @@ -#include "logging.hh" -#include "file-descriptor.hh" -#include "environment-variables.hh" -#include "terminal.hh" -#include "util.hh" -#include "config-global.hh" -#include "source-path.hh" -#include "position.hh" +#include "nix/util/logging.hh" +#include "nix/util/file-descriptor.hh" +#include "nix/util/environment-variables.hh" +#include "nix/util/terminal.hh" +#include "nix/util/util.hh" +#include "nix/util/config-global.hh" +#include "nix/util/source-path.hh" +#include "nix/util/position.hh" +#include "nix/util/sync.hh" +#include "nix/util/unix-domain-socket.hh" #include #include @@ -29,7 +31,7 @@ void setCurActivity(const ActivityId activityId) curActivity = activityId; } -Logger * logger = makeSimpleLogger(true); +std::unique_ptr logger = makeSimpleLogger(true); void Logger::warn(const std::string & msg) { @@ -43,6 +45,19 @@ void Logger::writeToStdout(std::string_view s) writeFull(standard_out, "\n"); } +Logger::Suspension Logger::suspend() +{ + pause(); + return Suspension { ._finalize = {[this](){this->resume();}} }; +} + +std::optional Logger::suspendIf(bool cond) +{ + if (cond) + return suspend(); + return {}; +} + class SimpleLogger : public Logger { public: @@ -128,9 +143,9 @@ void writeToStderr(std::string_view s) } } -Logger * makeSimpleLogger(bool printBuildLogs) +std::unique_ptr makeSimpleLogger(bool printBuildLogs) { - return new SimpleLogger(printBuildLogs); + return std::make_unique(printBuildLogs); } std::atomic nextId{0}; @@ -151,7 +166,7 @@ Activity::Activity(Logger & logger, Verbosity lvl, ActivityType type, logger.startActivity(id, lvl, type, s, fields, parent); } -void to_json(nlohmann::json & json, std::shared_ptr pos) +void to_json(nlohmann::json & json, std::shared_ptr pos) { if (pos) { json["line"] = pos->line; @@ -167,9 +182,13 @@ void to_json(nlohmann::json & json, std::shared_ptr pos) } struct JSONLogger : Logger { - Logger & prevLogger; + Descriptor fd; + bool includeNixPrefix; - JSONLogger(Logger & prevLogger) : prevLogger(prevLogger) { } + JSONLogger(Descriptor fd, bool includeNixPrefix) + : fd(fd) + , includeNixPrefix(includeNixPrefix) + { } bool isVerbose() override { return true; @@ -188,9 +207,33 @@ struct JSONLogger : Logger { unreachable(); } + struct State + { + bool enabled = true; + }; + + Sync _state; + void write(const nlohmann::json & json) { - prevLogger.log(lvlError, "@nix " + json.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace)); + auto line = + (includeNixPrefix ? "@nix " : "") + + json.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace); + + /* Acquire a lock to prevent log messages from clobbering each + other. */ + try { + auto state(_state.lock()); + if (state->enabled) + writeLine(fd, line); + } catch (...) { + bool enabled = false; + std::swap(_state.lock()->enabled, enabled); + if (enabled) { + ignoreExceptionExceptInterrupt(); + logger->warn("disabling JSON logger due to write errors"); + } + } } void log(Verbosity lvl, std::string_view s) override @@ -262,9 +305,49 @@ struct JSONLogger : Logger { } }; -Logger * makeJSONLogger(Logger & prevLogger) +std::unique_ptr makeJSONLogger(Descriptor fd, bool includeNixPrefix) +{ + return std::make_unique(fd, includeNixPrefix); +} + +std::unique_ptr makeJSONLogger(const std::filesystem::path & path, bool includeNixPrefix) +{ + struct JSONFileLogger : JSONLogger { + AutoCloseFD fd; + + JSONFileLogger(AutoCloseFD && fd, bool includeNixPrefix) + : JSONLogger(fd.get(), includeNixPrefix) + , fd(std::move(fd)) + { } + }; + + AutoCloseFD fd = + std::filesystem::is_socket(path) + ? connect(path) + : toDescriptor(open(path.string().c_str(), O_CREAT | O_APPEND | O_WRONLY, 0644)); + if (!fd) + throw SysError("opening log file %1%", path); + + return std::make_unique(std::move(fd), includeNixPrefix); +} + +void applyJSONLogger() { - return new JSONLogger(prevLogger); + if (!loggerSettings.jsonLogPath.get().empty()) { + try { + std::vector> loggers; + loggers.push_back(makeJSONLogger(std::filesystem::path(loggerSettings.jsonLogPath.get()), false)); + try { + logger = makeTeeLogger(std::move(logger), std::move(loggers)); + } catch (...) { + // `logger` is now gone so give up. + abort(); + } + } catch (...) { + ignoreExceptionExceptInterrupt(); + } + + } } static Logger::Fields getFields(nlohmann::json & json) @@ -280,61 +363,72 @@ static Logger::Fields getFields(nlohmann::json & json) return fields; } -std::optional parseJSONMessage(const std::string & msg) +std::optional parseJSONMessage(const std::string & msg, std::string_view source) { if (!hasPrefix(msg, "@nix ")) return std::nullopt; try { return nlohmann::json::parse(std::string(msg, 5)); } catch (std::exception & e) { - printError("bad JSON log message from builder: %s", e.what()); + printError("bad JSON log message from %s: %s", + Uncolored(source), + e.what()); } return std::nullopt; } bool handleJSONLogMessage(nlohmann::json & json, const Activity & act, std::map & activities, - bool trusted) + std::string_view source, bool trusted) { - std::string action = json["action"]; - - if (action == "start") { - auto type = (ActivityType) json["type"]; - if (trusted || type == actFileTransfer) - activities.emplace(std::piecewise_construct, - std::forward_as_tuple(json["id"]), - std::forward_as_tuple(*logger, (Verbosity) json["level"], type, - json["text"], getFields(json["fields"]), act.id)); - } + try { + std::string action = json["action"]; + + if (action == "start") { + auto type = (ActivityType) json["type"]; + if (trusted || type == actFileTransfer) + activities.emplace(std::piecewise_construct, + std::forward_as_tuple(json["id"]), + std::forward_as_tuple(*logger, (Verbosity) json["level"], type, + json["text"], getFields(json["fields"]), act.id)); + } - else if (action == "stop") - activities.erase((ActivityId) json["id"]); + else if (action == "stop") + activities.erase((ActivityId) json["id"]); - else if (action == "result") { - auto i = activities.find((ActivityId) json["id"]); - if (i != activities.end()) - i->second.result((ResultType) json["type"], getFields(json["fields"])); - } + else if (action == "result") { + auto i = activities.find((ActivityId) json["id"]); + if (i != activities.end()) + i->second.result((ResultType) json["type"], getFields(json["fields"])); + } - else if (action == "setPhase") { - std::string phase = json["phase"]; - act.result(resSetPhase, phase); - } + else if (action == "setPhase") { + std::string phase = json["phase"]; + act.result(resSetPhase, phase); + } - else if (action == "msg") { - std::string msg = json["msg"]; - logger->log((Verbosity) json["level"], msg); - } + else if (action == "msg") { + std::string msg = json["msg"]; + logger->log((Verbosity) json["level"], msg); + } - return true; + return true; + } catch (const nlohmann::json::exception &e) { + warn( + "Unable to handle a JSON message from %s: %s", + Uncolored(source), + e.what() + ); + return false; + } } bool handleJSONLogMessage(const std::string & msg, - const Activity & act, std::map & activities, bool trusted) + const Activity & act, std::map & activities, std::string_view source, bool trusted) { - auto json = parseJSONMessage(msg); + auto json = parseJSONMessage(msg, source); if (!json) return false; - return handleJSONLogMessage(*json, act, activities, trusted); + return handleJSONLogMessage(*json, act, activities, source, trusted); } Activity::~Activity() diff --git a/src/libutil/lru-cache.hh b/src/libutil/lru-cache.hh deleted file mode 100644 index 6e14cac3519..00000000000 --- a/src/libutil/lru-cache.hh +++ /dev/null @@ -1,104 +0,0 @@ -#pragma once -///@file - -#include -#include -#include -#include - -namespace nix { - -/** - * A simple least-recently used cache. Not thread-safe. - */ -template -class LRUCache -{ -private: - - size_t capacity; - - // Stupid wrapper to get around circular dependency between Data - // and LRU. - struct LRUIterator; - - using Data = std::map>; - using LRU = std::list; - - struct LRUIterator { typename LRU::iterator it; }; - - Data data; - LRU lru; - -public: - - LRUCache(size_t capacity) : capacity(capacity) { } - - /** - * Insert or upsert an item in the cache. - */ - void upsert(const Key & key, const Value & value) - { - if (capacity == 0) return; - - erase(key); - - if (data.size() >= capacity) { - /** - * Retire the oldest item. - */ - auto oldest = lru.begin(); - data.erase(*oldest); - lru.erase(oldest); - } - - auto res = data.emplace(key, std::make_pair(LRUIterator(), value)); - assert(res.second); - auto & i(res.first); - - auto j = lru.insert(lru.end(), i); - - i->second.first.it = j; - } - - bool erase(const Key & key) - { - auto i = data.find(key); - if (i == data.end()) return false; - lru.erase(i->second.first.it); - data.erase(i); - return true; - } - - /** - * Look up an item in the cache. If it exists, it becomes the most - * recently used item. - * */ - std::optional get(const Key & key) - { - auto i = data.find(key); - if (i == data.end()) return {}; - - /** - * Move this item to the back of the LRU list. - */ - lru.erase(i->second.first.it); - auto j = lru.insert(lru.end(), i); - i->second.first.it = j; - - return i->second.second; - } - - size_t size() const - { - return data.size(); - } - - void clear() - { - data.clear(); - lru.clear(); - } -}; - -} diff --git a/src/libutil/memory-source-accessor.cc b/src/libutil/memory-source-accessor.cc index c4eee1031cf..5612c9454f0 100644 --- a/src/libutil/memory-source-accessor.cc +++ b/src/libutil/memory-source-accessor.cc @@ -1,4 +1,4 @@ -#include "memory-source-accessor.hh" +#include "nix/util/memory-source-accessor.hh" namespace nix { @@ -187,6 +187,10 @@ void MemorySink::createSymlink(const CanonPath & path, const std::string & targe ref makeEmptySourceAccessor() { static auto empty = make_ref().cast(); + /* Don't forget to clear the display prefix, as the default constructed + SourceAccessor has the «unknown» prefix. Since this accessor is supposed + to mimic an empty root directory the prefix needs to be empty. */ + empty->setPathDisplay(""); return empty; } diff --git a/src/libutil/meson.build b/src/libutil/meson.build index 11b4ea59243..f48c8f3d72b 100644 --- a/src/libutil/meson.build +++ b/src/libutil/meson.build @@ -4,8 +4,6 @@ project('nix-util', 'cpp', 'cpp_std=c++2a', # TODO(Qyriad): increase the warning level 'warning_level=1', - 'debug=true', - 'optimization=2', 'errorlogs=true', # Please print logs for tests that fail ], meson_version : '>= 1.1', @@ -14,7 +12,7 @@ project('nix-util', 'cpp', cxx = meson.get_compiler('cpp') -subdir('build-utils-meson/deps-lists') +subdir('nix-meson-build-support/deps-lists') configdata = configuration_data() @@ -22,38 +20,24 @@ deps_private_maybe_subproject = [ ] deps_public_maybe_subproject = [ ] -subdir('build-utils-meson/subprojects') +subdir('nix-meson-build-support/subprojects') # Check for each of these functions, and create a define like `#define -# HAVE_LUTIMES 1`. The `#define` is unconditional, 0 for not found and 1 -# for found. One therefore uses it with `#if` not `#ifdef`. +# HAVE_POSIX_FALLOCATE 1`. The `#define` is unconditional, 0 for not +# found and 1 for found. One therefore uses it with `#if` not `#ifdef`. check_funcs = [ - 'close_range', - # Optionally used for changing the mtime of symlinks. - 'lutimes', - # Optionally used for creating pipes on Unix - 'pipe2', - # Optionally used to preallocate files to be large enough before - # writing to them. - 'posix_fallocate', - # Optionally used to get more information about processes failing due - # to a signal on Unix. - 'strsignal', - # Optionally used to try to close more file descriptors (e.g. before - # forking) on Unix. - 'sysconf', - # Optionally used for changing the mtime of files and symlinks. - 'utimensat', + [ + 'posix_fallocate', + 'Optionally used to preallocate files to be large enough before writing to them.', + ], ] foreach funcspec : check_funcs - define_name = 'HAVE_' + funcspec.underscorify().to_upper() - define_value = cxx.has_function(funcspec).to_int() - configdata.set(define_name, define_value) + define_name = 'HAVE_' + funcspec[0].underscorify().to_upper() + define_value = cxx.has_function(funcspec[0]).to_int() + configdata.set(define_name, define_value, description: funcspec[1]) endforeach -configdata.set('HAVE_DECL_AT_SYMLINK_NOFOLLOW', cxx.has_header_symbol('fcntl.h', 'AT_SYMLINK_NOFOLLOW').to_int()) - -subdir('build-utils-meson/libatomic') +subdir('nix-meson-build-support/libatomic') if host_machine.system() == 'windows' socket = cxx.find_library('ws2_32') @@ -64,10 +48,18 @@ elif host_machine.system() == 'sunos' deps_other += [socket, network_service_library] endif +blake3 = dependency( + 'libblake3', + version: '>= 1.8.2', + method : 'pkg-config', +) +deps_private += blake3 + boost = dependency( 'boost', - modules : ['context', 'coroutine'], + modules : ['context', 'coroutine', 'iostreams'], include_type: 'system', + version: '>=1.82.0' ) # boost is a public dependency, but not a pkg-config dependency unfortunately, so we # put in `deps_other`. @@ -101,34 +93,29 @@ cpuid_required = get_option('cpuid') if host_machine.cpu_family() != 'x86_64' and cpuid_required.enabled() warning('Force-enabling seccomp on non-x86_64 does not make sense') endif -cpuid = dependency('libcpuid', 'cpuid', required : cpuid_required) +cpuid = dependency('libcpuid', 'cpuid', version : '>= 0.7.0', required : cpuid_required) configdata.set('HAVE_LIBCPUID', cpuid.found().to_int()) deps_private += cpuid nlohmann_json = dependency('nlohmann_json', version : '>= 3.9') deps_public += nlohmann_json -config_h = configure_file( - configuration : configdata, - output : 'config-util.hh', -) +cxx = meson.get_compiler('cpp') -add_project_arguments( - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. - '-include', 'config-util.hh', - language : 'cpp', +config_priv_h = configure_file( + configuration : configdata, + output : 'util-config-private.hh', ) -subdir('build-utils-meson/common') +subdir('nix-meson-build-support/common') -sources = files( +sources = [config_priv_h] + files( 'archive.cc', 'args.cc', 'canon-path.cc', 'compression.cc', 'compute-levels.cc', - 'config.cc', + 'configuration.cc', 'config-global.cc', 'current-process.cc', 'english.cc', @@ -147,7 +134,9 @@ sources = files( 'json-utils.cc', 'logging.cc', 'memory-source-accessor.cc', + 'mounted-source-accessor.cc', 'position.cc', + 'pos-table.cc', 'posix-source-accessor.cc', 'references.cc', 'serialise.cc', @@ -155,11 +144,14 @@ sources = files( 'signature/signer.cc', 'source-accessor.cc', 'source-path.cc', + 'subdir-source-accessor.cc', 'strings.cc', 'suggestions.cc', 'tarfile.cc', + 'tee-logger.cc', 'terminal.cc', 'thread-pool.cc', + 'union-source-accessor.cc', 'unix-domain-socket.cc', 'url.cc', 'users.cc', @@ -167,97 +159,29 @@ sources = files( 'xml-writer.cc', ) -include_dirs = [include_directories('.')] +subdir('include/nix/util') -headers = [config_h] + files( - 'abstract-setting-to-json.hh', - 'ansicolor.hh', - 'archive.hh', - 'args.hh', - 'args/root.hh', - 'callback.hh', - 'canon-path.hh', - 'checked-arithmetic.hh', - 'chunked-vector.hh', - 'closure.hh', - 'comparator.hh', - 'compression.hh', - 'compute-levels.hh', - 'config-global.hh', - 'config-impl.hh', - 'config.hh', - 'current-process.hh', - 'english.hh', - 'environment-variables.hh', - 'error.hh', - 'exec.hh', - 'executable-path.hh', - 'exit.hh', - 'experimental-features.hh', - 'file-content-address.hh', - 'file-descriptor.hh', - 'file-path-impl.hh', - 'file-path.hh', - 'file-system.hh', - 'finally.hh', - 'fmt.hh', - 'fs-sink.hh', - 'git.hh', - 'hash.hh', - 'hilite.hh', - 'json-impls.hh', - 'json-utils.hh', - 'logging.hh', - 'lru-cache.hh', - 'memory-source-accessor.hh', - 'muxable-pipe.hh', - 'os-string.hh', - 'pool.hh', - 'position.hh', - 'posix-source-accessor.hh', - 'processes.hh', - 'ref.hh', - 'references.hh', - 'regex-combinators.hh', - 'repair-flag.hh', - 'serialise.hh', - 'signals.hh', - 'signature/local-keys.hh', - 'signature/signer.hh', - 'source-accessor.hh', - 'source-path.hh', - 'split.hh', - 'std-hash.hh', - 'strings.hh', - 'strings-inline.hh', - 'suggestions.hh', - 'sync.hh', - 'tarfile.hh', - 'terminal.hh', - 'thread-pool.hh', - 'topo-sort.hh', - 'types.hh', - 'unix-domain-socket.hh', - 'url-parts.hh', - 'url.hh', - 'users.hh', - 'util.hh', - 'variant-wrapper.hh', - 'xml-writer.hh', -) +if not cxx.has_header('widechar_width.h', required : false) + # use vendored widechar_width.h + include_dirs += include_directories('./widecharwidth') +endif if host_machine.system() == 'linux' subdir('linux') endif +if host_machine.system() == 'freebsd' + subdir('freebsd') +endif + if host_machine.system() == 'windows' subdir('windows') else subdir('unix') endif -subdir('build-utils-meson/export-all-symbols') -subdir('build-utils-meson/windows-version') +subdir('nix-meson-build-support/export-all-symbols') +subdir('nix-meson-build-support/windows-version') this_library = library( 'nixutil', @@ -269,7 +193,7 @@ this_library = library( install : true, ) -install_headers(headers, subdir : 'nix', preserve_path : true) +install_headers(headers, subdir : 'nix/util', preserve_path : true) libraries_private = [] if host_machine.system() == 'windows' @@ -278,4 +202,4 @@ if host_machine.system() == 'windows' libraries_private += ['-lws2_32'] endif -subdir('build-utils-meson/export') +subdir('nix-meson-build-support/export') diff --git a/src/libfetchers/mounted-source-accessor.cc b/src/libutil/mounted-source-accessor.cc similarity index 92% rename from src/libfetchers/mounted-source-accessor.cc rename to src/libutil/mounted-source-accessor.cc index 68f3a546b57..b7de2afbf03 100644 --- a/src/libfetchers/mounted-source-accessor.cc +++ b/src/libutil/mounted-source-accessor.cc @@ -1,4 +1,4 @@ -#include "mounted-source-accessor.hh" +#include "nix/util/source-accessor.hh" namespace nix { @@ -23,12 +23,6 @@ struct MountedSourceAccessor : SourceAccessor return accessor->readFile(subpath); } - bool pathExists(const CanonPath & path) override - { - auto [accessor, subpath] = resolve(path); - return accessor->pathExists(subpath); - } - std::optional maybeLstat(const CanonPath & path) override { auto [accessor, subpath] = resolve(path); @@ -69,6 +63,12 @@ struct MountedSourceAccessor : SourceAccessor path.pop(); } } + + std::optional getPhysicalPath(const CanonPath & path) override + { + auto [accessor, subpath] = resolve(path); + return accessor->getPhysicalPath(subpath); + } }; ref makeMountedSourceAccessor(std::map> mounts) diff --git a/src/libutil/nix-meson-build-support b/src/libutil/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/libutil/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/libutil/package.nix b/src/libutil/package.nix index 17a15674008..46f56e07e6d 100644 --- a/src/libutil/package.nix +++ b/src/libutil/package.nix @@ -1,18 +1,20 @@ -{ lib -, stdenv -, mkMesonLibrary +{ + lib, + stdenv, + mkMesonLibrary, -, boost -, brotli -, libarchive -, libcpuid -, libsodium -, nlohmann_json -, openssl + boost, + brotli, + libarchive, + libblake3, + libcpuid, + libsodium, + nlohmann_json, + openssl, -# Configuration Options + # Configuration Options -, version + version, }: let @@ -25,25 +27,32 @@ mkMesonLibrary (finalAttrs: { workDir = ./.; fileset = fileset.unions [ - ../../build-utils-meson - ./build-utils-meson + ../../nix-meson-build-support + ./nix-meson-build-support ../../.version ./.version + ./widecharwidth ./meson.build ./meson.options + ./include/nix/util/meson.build ./linux/meson.build + ./linux/include/nix/util/meson.build + ./freebsd/meson.build + ./freebsd/include/nix/util/meson.build ./unix/meson.build + ./unix/include/nix/util/meson.build ./windows/meson.build + ./windows/include/nix/util/meson.build (fileset.fileFilter (file: file.hasExt "cc") ./.) (fileset.fileFilter (file: file.hasExt "hh") ./.) ]; buildInputs = [ brotli + libblake3 libsodium openssl - ] ++ lib.optional stdenv.hostPlatform.isx86_64 libcpuid - ; + ] ++ lib.optional stdenv.hostPlatform.isx86_64 libcpuid; propagatedBuildInputs = [ boost @@ -51,30 +60,10 @@ mkMesonLibrary (finalAttrs: { nlohmann_json ]; - preConfigure = - # "Inline" .version so it's not a symlink, and includes the suffix. - # Do the meson utils, without modification. - # - # TODO: change release process to add `pre` in `.version`, remove it - # before tagging, and restore after. - '' - chmod u+w ./.version - echo ${version} > ../../.version - ''; - mesonFlags = [ (lib.mesonEnable "cpuid" stdenv.hostPlatform.isx86_64) ]; - env = { - # Needed for Meson to find Boost. - # https://github.com/NixOS/nixpkgs/issues/86131. - BOOST_INCLUDEDIR = "${lib.getDev boost}/include"; - BOOST_LIBRARYDIR = "${lib.getLib boost}/lib"; - } // lib.optionalAttrs (stdenv.isLinux && !(stdenv.hostPlatform.isStatic && stdenv.system == "aarch64-linux")) { - LDFLAGS = "-fuse-ld=gold"; - }; - meta = { platforms = lib.platforms.unix ++ lib.platforms.windows; }; diff --git a/src/libutil/pos-table.cc b/src/libutil/pos-table.cc new file mode 100644 index 00000000000..e50b1287317 --- /dev/null +++ b/src/libutil/pos-table.cc @@ -0,0 +1,51 @@ +#include "nix/util/pos-table.hh" + +#include + +namespace nix { + +/* Position table. */ + +Pos PosTable::operator[](PosIdx p) const +{ + auto origin = resolve(p); + if (!origin) + return {}; + + const auto offset = origin->offsetOf(p); + + Pos result{0, 0, origin->origin}; + auto linesCache = this->linesCache.lock(); + + /* Try the origin's line cache */ + const auto * linesForInput = linesCache->getOrNullptr(origin->offset); + + auto fillCacheForOrigin = [](std::string_view content) { + auto contentLines = Lines(); + + const char * begin = content.data(); + for (Pos::LinesIterator it(content), end; it != end; it++) + contentLines.push_back(it->data() - begin); + if (contentLines.empty()) + contentLines.push_back(0); + + return contentLines; + }; + + /* Calculate line offsets and fill the cache */ + if (!linesForInput) { + auto originContent = result.getSource().value_or(""); + linesCache->upsert(origin->offset, fillCacheForOrigin(originContent)); + linesForInput = linesCache->getOrNullptr(origin->offset); + } + + assert(linesForInput); + + // as above: the first line starts at byte 0 and is always present + auto lineStartOffset = std::prev(std::upper_bound(linesForInput->begin(), linesForInput->end(), offset)); + result.line = 1 + (lineStartOffset - linesForInput->begin()); + result.column = 1 + (offset - *lineStartOffset); + return result; +} + +} diff --git a/src/libutil/position.cc b/src/libutil/position.cc index 946f167b611..a1d9460ed34 100644 --- a/src/libutil/position.cc +++ b/src/libutil/position.cc @@ -1,20 +1,10 @@ -#include "position.hh" +#include "nix/util/position.hh" namespace nix { -Pos::Pos(const Pos * other) +Pos::operator std::shared_ptr() const { - if (!other) { - return; - } - line = other->line; - column = other->column; - origin = other->origin; -} - -Pos::operator std::shared_ptr() const -{ - return std::make_shared(&*this); + return std::make_shared(*this); } std::optional Pos::getCodeLines() const @@ -66,6 +56,13 @@ std::optional Pos::getSource() const }, origin); } +std::optional Pos::getSourcePath() const +{ + if (auto * path = std::get_if(&origin)) + return *path; + return std::nullopt; +} + void Pos::print(std::ostream & out, bool showOrigin) const { if (showOrigin) { diff --git a/src/libutil/posix-source-accessor.cc b/src/libutil/posix-source-accessor.cc index 50b43689353..2ce7c88e4f8 100644 --- a/src/libutil/posix-source-accessor.cc +++ b/src/libutil/posix-source-accessor.cc @@ -1,7 +1,7 @@ -#include "posix-source-accessor.hh" -#include "source-path.hh" -#include "signals.hh" -#include "sync.hh" +#include "nix/util/posix-source-accessor.hh" +#include "nix/util/source-path.hh" +#include "nix/util/signals.hh" +#include "nix/util/sync.hh" #include @@ -122,7 +122,13 @@ std::optional PosixSourceAccessor::maybeLstat(const CanonP S_ISREG(st->st_mode) ? tRegular : S_ISDIR(st->st_mode) ? tDirectory : S_ISLNK(st->st_mode) ? tSymlink : - tMisc, + S_ISCHR(st->st_mode) ? tChar : + S_ISBLK(st->st_mode) ? tBlock : +#ifdef S_ISSOCK + S_ISSOCK(st->st_mode) ? tSocket : +#endif + S_ISFIFO(st->st_mode) ? tFifo : + tUnknown, .fileSize = S_ISREG(st->st_mode) ? std::optional(st->st_size) : std::nullopt, .isExecutable = S_ISREG(st->st_mode) && st->st_mode & S_IXUSR, }; @@ -132,38 +138,49 @@ SourceAccessor::DirEntries PosixSourceAccessor::readDirectory(const CanonPath & { assertNoSymlinks(path); DirEntries res; - try { - for (auto & entry : std::filesystem::directory_iterator{makeAbsPath(path)}) { - checkInterrupt(); - auto type = [&]() -> std::optional { - std::filesystem::file_type nativeType; - try { - nativeType = entry.symlink_status().type(); - } catch (std::filesystem::filesystem_error & e) { - // We cannot always stat the child. (Ideally there is no - // stat because the native directory entry has the type - // already, but this isn't always the case.) - if (e.code() == std::errc::permission_denied || e.code() == std::errc::operation_not_permitted) - return std::nullopt; - else throw; - } - - // cannot exhaustively enumerate because implementation-specific - // additional file types are allowed. -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wswitch-enum" - switch (nativeType) { - case std::filesystem::file_type::regular: return Type::tRegular; break; - case std::filesystem::file_type::symlink: return Type::tSymlink; break; - case std::filesystem::file_type::directory: return Type::tDirectory; break; - default: return tMisc; + for (auto & entry : DirectoryIterator{makeAbsPath(path)}) { + checkInterrupt(); + auto type = [&]() -> std::optional { + try { + /* WARNING: We are specifically not calling symlink_status() + * here, because that always translates to `stat` call and + * doesn't make use of any caching. Instead, we have to + * rely on the myriad of `is_*` functions, which actually do + * the caching. If you are in doubt then take a look at the + * libstdc++ implementation [1] and the standard proposal + * about the caching variations of directory_entry [2]. + + * [1]: https://github.com/gcc-mirror/gcc/blob/8ea555b7b4725dbc5d9286f729166cd54ce5b615/libstdc%2B%2B-v3/include/bits/fs_dir.h#L341-L348 + * [2]: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0317r1.html + */ + + /* Check for symlink first, because other getters follow symlinks. */ + if (entry.is_symlink()) + return tSymlink; + if (entry.is_regular_file()) + return tRegular; + if (entry.is_directory()) + return tDirectory; + if (entry.is_character_file()) + return tChar; + if (entry.is_block_file()) + return tBlock; + if (entry.is_fifo()) + return tFifo; + if (entry.is_socket()) + return tSocket; + return tUnknown; + } catch (std::filesystem::filesystem_error & e) { + // We cannot always stat the child. (Ideally there is no + // stat because the native directory entry has the type + // already, but this isn't always the case.) + if (e.code() == std::errc::permission_denied || e.code() == std::errc::operation_not_permitted) + return std::nullopt; + else + throw; } -#pragma GCC diagnostic pop - }(); - res.emplace(entry.path().filename().string(), type); - } - } catch (std::filesystem::filesystem_error & e) { - throw SysError("reading directory %1%", showPath(path)); + }(); + res.emplace(entry.path().filename().string(), type); } return res; } diff --git a/src/libutil/references.cc b/src/libutil/references.cc index b30e62c7b2b..66ad9d37cca 100644 --- a/src/libutil/references.cc +++ b/src/libutil/references.cc @@ -1,6 +1,6 @@ -#include "references.hh" -#include "hash.hh" -#include "archive.hh" +#include "nix/util/references.hh" +#include "nix/util/hash.hh" +#include "nix/util/archive.hh" #include #include diff --git a/src/libutil/serialise.cc b/src/libutil/serialise.cc index 381e7ae3825..a7453158256 100644 --- a/src/libutil/serialise.cc +++ b/src/libutil/serialise.cc @@ -1,6 +1,6 @@ -#include "serialise.hh" -#include "signals.hh" -#include "util.hh" +#include "nix/util/serialise.hh" +#include "nix/util/signals.hh" +#include "nix/util/util.hh" #include #include @@ -11,7 +11,7 @@ #ifdef _WIN32 # include # include -# include "windows-error.hh" +# include "nix/util/windows-error.hh" #else # include #endif @@ -194,10 +194,6 @@ size_t StringSource::read(char * data, size_t len) } -#if BOOST_VERSION >= 106300 && BOOST_VERSION < 106600 -#error Coroutines are broken in this version of Boost! -#endif - std::unique_ptr sourceToSink(std::function fun) { struct SourceToSink : FinishSink @@ -227,8 +223,7 @@ std::unique_ptr sourceToSink(std::function fun) throw EndOfFile("coroutine has finished"); } - size_t n = std::min(cur.size(), out_len); - memcpy(out, cur.data(), n); + size_t n = cur.copy(out, out_len); cur.remove_prefix(n); return n; }); @@ -260,7 +255,7 @@ std::unique_ptr sinkToSource( { struct SinkToSource : Source { - typedef boost::coroutines2::coroutine coro_t; + typedef boost::coroutines2::coroutine coro_t; std::function fun; std::function eof; @@ -271,33 +266,37 @@ std::unique_ptr sinkToSource( { } - std::string cur; - size_t pos = 0; + std::string_view cur; size_t read(char * data, size_t len) override { - if (!coro) { + bool hasCoro = coro.has_value(); + if (!hasCoro) { coro = coro_t::pull_type([&](coro_t::push_type & yield) { LambdaSink sink([&](std::string_view data) { - if (!data.empty()) yield(std::string(data)); + if (!data.empty()) { + yield(data); + } }); fun(sink); }); } - if (!*coro) { eof(); unreachable(); } - - if (pos == cur.size()) { - if (!cur.empty()) { + if (cur.empty()) { + if (hasCoro) { (*coro)(); } - cur = coro->get(); - pos = 0; + if (*coro) { + cur = coro->get(); + } else { + coro.reset(); + eof(); + unreachable(); + } } - auto n = std::min(cur.size() - pos, len); - memcpy(data, cur.data() + pos, n); - pos += n; + size_t n = cur.copy(data, len); + cur.remove_prefix(n); return n; } diff --git a/src/libutil/signature/local-keys.cc b/src/libutil/signature/local-keys.cc index 70bcb5f33c2..1f7f2c7de14 100644 --- a/src/libutil/signature/local-keys.cc +++ b/src/libutil/signature/local-keys.cc @@ -1,7 +1,7 @@ -#include "signature/local-keys.hh" +#include "nix/util/signature/local-keys.hh" -#include "file-system.hh" -#include "util.hh" +#include "nix/util/file-system.hh" +#include "nix/util/util.hh" #include namespace nix { diff --git a/src/libutil/signature/signer.cc b/src/libutil/signature/signer.cc index 0d26867b54a..46445e9e983 100644 --- a/src/libutil/signature/signer.cc +++ b/src/libutil/signature/signer.cc @@ -1,5 +1,5 @@ -#include "signature/signer.hh" -#include "error.hh" +#include "nix/util/signature/signer.hh" +#include "nix/util/error.hh" #include diff --git a/src/libutil/source-accessor.cc b/src/libutil/source-accessor.cc index d3e304f740d..fc9752456a1 100644 --- a/src/libutil/source-accessor.cc +++ b/src/libutil/source-accessor.cc @@ -1,10 +1,30 @@ -#include "source-accessor.hh" -#include "archive.hh" +#include +#include "nix/util/source-accessor.hh" namespace nix { static std::atomic nextNumber{0}; +bool SourceAccessor::Stat::isNotNARSerialisable() +{ + return this->type != tRegular && this->type != tSymlink && this->type != tDirectory; +} + +std::string SourceAccessor::Stat::typeString() { + switch (this->type) { + case tRegular: return "regular"; + case tSymlink: return "symlink"; + case tDirectory: return "directory"; + case tChar: return "character device"; + case tBlock: return "block device"; + case tSocket: return "socket"; + case tFifo: return "fifo"; + case tUnknown: + default: return "unknown"; + } + return "unknown"; +} + SourceAccessor::SourceAccessor() : number(++nextNumber) , displayPrefix{"«unknown»"} @@ -94,9 +114,11 @@ CanonPath SourceAccessor::resolveSymlinks( if (!linksAllowed--) throw Error("infinite symlink recursion in path '%s'", showPath(path)); auto target = readLink(res); - res.pop(); - if (hasPrefix(target, "/")) + if (isAbsolute(target)) { res = CanonPath::root; + } else { + res.pop(); + } todo.splice(todo.begin(), tokenizeString>(target, "/")); } } diff --git a/src/libutil/source-path.cc b/src/libutil/source-path.cc index 759d3c35579..6d42fa95fe5 100644 --- a/src/libutil/source-path.cc +++ b/src/libutil/source-path.cc @@ -1,4 +1,4 @@ -#include "source-path.hh" +#include "nix/util/source-path.hh" namespace nix { diff --git a/src/libutil/strings.cc b/src/libutil/strings.cc index c221a43c6f1..a953900891e 100644 --- a/src/libutil/strings.cc +++ b/src/libutil/strings.cc @@ -2,8 +2,9 @@ #include #include -#include "strings-inline.hh" -#include "os-string.hh" +#include "nix/util/strings-inline.hh" +#include "nix/util/os-string.hh" +#include "nix/util/error.hh" namespace nix { @@ -16,26 +17,29 @@ struct view_stringbuf : public std::stringbuf } }; -std::string_view toView(const std::ostringstream & os) +__attribute__((no_sanitize("undefined"))) std::string_view toView(const std::ostringstream & os) { + /* Downcasting like this is very much undefined behavior, so we disable + UBSAN for this function. */ auto buf = static_cast(os.rdbuf()); return buf->toView(); } template std::list tokenizeString(std::string_view s, std::string_view separators); -template std::set tokenizeString(std::string_view s, std::string_view separators); +template StringSet tokenizeString(std::string_view s, std::string_view separators); template std::vector tokenizeString(std::string_view s, std::string_view separators); template std::list splitString(std::string_view s, std::string_view separators); -template std::set splitString(std::string_view s, std::string_view separators); +template StringSet splitString(std::string_view s, std::string_view separators); template std::vector splitString(std::string_view s, std::string_view separators); template std::list basicSplitString(std::basic_string_view s, std::basic_string_view separators); template std::string concatStringsSep(std::string_view, const std::list &); -template std::string concatStringsSep(std::string_view, const std::set &); +template std::string concatStringsSep(std::string_view, const StringSet &); template std::string concatStringsSep(std::string_view, const std::vector &); +template std::string concatStringsSep(std::string_view, const boost::container::small_vector &); typedef std::string_view strings_2[2]; template std::string concatStringsSep(std::string_view, const strings_2 &); @@ -45,7 +49,110 @@ typedef std::string_view strings_4[4]; template std::string concatStringsSep(std::string_view, const strings_4 &); template std::string dropEmptyInitThenConcatStringsSep(std::string_view, const std::list &); -template std::string dropEmptyInitThenConcatStringsSep(std::string_view, const std::set &); +template std::string dropEmptyInitThenConcatStringsSep(std::string_view, const StringSet &); template std::string dropEmptyInitThenConcatStringsSep(std::string_view, const std::vector &); +/** + * Shell split string: split a string into shell arguments, respecting quotes and backslashes. + * + * Used for NIX_SSHOPTS handling, which previously used `tokenizeString` and was broken by + * Arguments that need to be passed to ssh with spaces in them. + * + * Read https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html for the + * POSIX shell specification, which is technically what we are implementing here. + */ +std::list shellSplitString(std::string_view s) +{ + std::list result; + std::string current; + bool startedCurrent = false; + bool escaping = false; + + auto pushCurrent = [&]() { + if (startedCurrent) { + result.push_back(current); + current.clear(); + startedCurrent = false; + } + }; + + auto pushChar = [&](char c) { + current.push_back(c); + startedCurrent = true; + }; + + auto pop = [&]() { + auto c = s[0]; + s.remove_prefix(1); + return c; + }; + + auto inDoubleQuotes = [&]() { + startedCurrent = true; + // in double quotes, escaping with backslash is only effective for $, `, ", and backslash + while (!s.empty()) { + auto c = pop(); + if (escaping) { + switch (c) { + case '$': + case '`': + case '"': + case '\\': + pushChar(c); + break; + default: + pushChar('\\'); + pushChar(c); + break; + } + escaping = false; + } else if (c == '\\') { + escaping = true; + } else if (c == '"') { + return; + } else { + pushChar(c); + } + } + if (s.empty()) { + throw Error("unterminated double quote"); + } + }; + + auto inSingleQuotes = [&]() { + startedCurrent = true; + while (!s.empty()) { + auto c = pop(); + if (c == '\'') { + return; + } + pushChar(c); + } + if (s.empty()) { + throw Error("unterminated single quote"); + } + }; + + while (!s.empty()) { + auto c = pop(); + if (escaping) { + pushChar(c); + escaping = false; + } else if (c == '\\') { + escaping = true; + } else if (c == ' ' || c == '\t') { + pushCurrent(); + } else if (c == '"') { + inDoubleQuotes(); + } else if (c == '\'') { + inSingleQuotes(); + } else { + pushChar(c); + } + } + + pushCurrent(); + + return result; +} } // namespace nix diff --git a/src/libutil/subdir-source-accessor.cc b/src/libutil/subdir-source-accessor.cc new file mode 100644 index 00000000000..2658361188a --- /dev/null +++ b/src/libutil/subdir-source-accessor.cc @@ -0,0 +1,59 @@ +#include "nix/util/source-accessor.hh" + +namespace nix { + +struct SubdirSourceAccessor : SourceAccessor +{ + ref parent; + + CanonPath subdirectory; + + SubdirSourceAccessor(ref && parent, CanonPath && subdirectory) + : parent(std::move(parent)) + , subdirectory(std::move(subdirectory)) + { + displayPrefix.clear(); + } + + std::string readFile(const CanonPath & path) override + { + return parent->readFile(subdirectory / path); + } + + void readFile(const CanonPath & path, Sink & sink, std::function sizeCallback) override + { + return parent->readFile(subdirectory / path, sink, sizeCallback); + } + + bool pathExists(const CanonPath & path) override + { + return parent->pathExists(subdirectory / path); + } + + std::optional maybeLstat(const CanonPath & path) override + { + return parent->maybeLstat(subdirectory / path); + } + + DirEntries readDirectory(const CanonPath & path) override + { + return parent->readDirectory(subdirectory / path); + } + + std::string readLink(const CanonPath & path) override + { + return parent->readLink(subdirectory / path); + } + + std::string showPath(const CanonPath & path) override + { + return displayPrefix + parent->showPath(subdirectory / path) + displaySuffix; + } +}; + +ref projectSubdirSourceAccessor(ref parent, CanonPath subdirectory) +{ + return make_ref(std::move(parent), std::move(subdirectory)); +} + +} diff --git a/src/libutil/suggestions.cc b/src/libutil/suggestions.cc index 84c8e296f17..aee23d45e41 100644 --- a/src/libutil/suggestions.cc +++ b/src/libutil/suggestions.cc @@ -1,6 +1,6 @@ -#include "suggestions.hh" -#include "ansicolor.hh" -#include "terminal.hh" +#include "nix/util/suggestions.hh" +#include "nix/util/ansicolor.hh" +#include "nix/util/terminal.hh" #include #include @@ -38,7 +38,7 @@ int levenshteinDistance(std::string_view first, std::string_view second) } Suggestions Suggestions::bestMatches ( - const std::set & allMatches, + const StringSet & allMatches, std::string_view query) { std::set res; diff --git a/src/libutil/tarfile.cc b/src/libutil/tarfile.cc index e412930bb67..299847850b0 100644 --- a/src/libutil/tarfile.cc +++ b/src/libutil/tarfile.cc @@ -1,17 +1,13 @@ #include #include -#include "finally.hh" -#include "serialise.hh" -#include "tarfile.hh" -#include "file-system.hh" +#include "nix/util/finally.hh" +#include "nix/util/serialise.hh" +#include "nix/util/tarfile.hh" +#include "nix/util/file-system.hh" namespace nix { -namespace fs { -using namespace std::filesystem; -} - namespace { int callback_open(struct archive *, void * self) @@ -127,7 +123,7 @@ TarArchive::~TarArchive() archive_read_free(this->archive); } -static void extract_archive(TarArchive & archive, const fs::path & destDir) +static void extract_archive(TarArchive & archive, const std::filesystem::path & destDir) { int flags = ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_SECURE_SYMLINKS | ARCHIVE_EXTRACT_SECURE_NODOTDOT; @@ -162,19 +158,19 @@ static void extract_archive(TarArchive & archive, const fs::path & destDir) archive.close(); } -void unpackTarfile(Source & source, const fs::path & destDir) +void unpackTarfile(Source & source, const std::filesystem::path & destDir) { auto archive = TarArchive(source); - fs::create_directories(destDir); + createDirs(destDir); extract_archive(archive, destDir); } -void unpackTarfile(const fs::path & tarFile, const fs::path & destDir) +void unpackTarfile(const std::filesystem::path & tarFile, const std::filesystem::path & destDir) { auto archive = TarArchive(tarFile); - fs::create_directories(destDir); + createDirs(destDir); extract_archive(archive, destDir); } @@ -182,6 +178,10 @@ time_t unpackTarfileToSink(TarArchive & archive, ExtendedFileSystemObjectSink & { time_t lastModified = 0; + /* Only allocate the buffer once. Use the heap because 131 KiB is a bit too + much for the stack. */ + std::vector buf(128 * 1024); + for (;;) { // FIXME: merge with extract_archive struct archive_entry * entry; @@ -216,10 +216,9 @@ time_t unpackTarfileToSink(TarArchive & archive, ExtendedFileSystemObjectSink & crf.isExecutable(); while (true) { - std::vector buf(128 * 1024); auto n = archive_read_data(archive.archive, buf.data(), buf.size()); if (n < 0) - throw Error("cannot read file '%s' from tarball", path); + checkLibArchive(archive.archive, n, "cannot read file from tarball: %s"); if (n == 0) break; crf(std::string_view{ diff --git a/src/libutil/tee-logger.cc b/src/libutil/tee-logger.cc new file mode 100644 index 00000000000..55334a821fb --- /dev/null +++ b/src/libutil/tee-logger.cc @@ -0,0 +1,107 @@ +#include "nix/util/logging.hh" + +namespace nix { + +struct TeeLogger : Logger +{ + std::vector> loggers; + + TeeLogger(std::vector> && loggers) + : loggers(std::move(loggers)) + { + } + + void stop() override + { + for (auto & logger : loggers) + logger->stop(); + }; + + void pause() override + { + for (auto & logger : loggers) + logger->pause(); + }; + + void resume() override + { + for (auto & logger : loggers) + logger->resume(); + }; + + void log(Verbosity lvl, std::string_view s) override + { + for (auto & logger : loggers) + logger->log(lvl, s); + } + + void logEI(const ErrorInfo & ei) override + { + for (auto & logger : loggers) + logger->logEI(ei); + } + + void startActivity( + ActivityId act, + Verbosity lvl, + ActivityType type, + const std::string & s, + const Fields & fields, + ActivityId parent) override + { + for (auto & logger : loggers) + logger->startActivity(act, lvl, type, s, fields, parent); + } + + void stopActivity(ActivityId act) override + { + for (auto & logger : loggers) + logger->stopActivity(act); + } + + void result(ActivityId act, ResultType type, const Fields & fields) override + { + for (auto & logger : loggers) + logger->result(act, type, fields); + } + + void writeToStdout(std::string_view s) override + { + for (auto & logger : loggers) { + /* Let only the first logger write to stdout to avoid + duplication. This means that the first logger needs to + be the one managing stdout/stderr + (e.g. `ProgressBar`). */ + logger->writeToStdout(s); + break; + } + } + + std::optional ask(std::string_view s) override + { + for (auto & logger : loggers) { + auto c = logger->ask(s); + if (c) + return c; + } + return std::nullopt; + } + + void setPrintBuildLogs(bool printBuildLogs) override + { + for (auto & logger : loggers) + logger->setPrintBuildLogs(printBuildLogs); + } +}; + +std::unique_ptr +makeTeeLogger(std::unique_ptr mainLogger, std::vector> && extraLoggers) +{ + std::vector> allLoggers; + allLoggers.push_back(std::move(mainLogger)); + for (auto & l : extraLoggers) + allLoggers.push_back(std::move(l)); + return std::make_unique(std::move(allLoggers)); +} + +} diff --git a/src/libutil/terminal.cc b/src/libutil/terminal.cc index 4c127ddb078..63473d1a957 100644 --- a/src/libutil/terminal.cc +++ b/src/libutil/terminal.cc @@ -1,8 +1,8 @@ -#include "terminal.hh" -#include "environment-variables.hh" -#include "sync.hh" +#include "nix/util/terminal.hh" +#include "nix/util/environment-variables.hh" +#include "nix/util/sync.hh" -#if _WIN32 +#ifdef _WIN32 # include # define WIN32_LEAN_AND_MEAN # include @@ -11,6 +11,53 @@ # include #endif #include +#include + +namespace { + +inline std::pair charWidthUTF8Helper(std::string_view s) +{ + size_t bytes = 1; + uint32_t ch = s[0]; + uint32_t max = 1U << 7; + if ((ch & 0x80U) == 0U) { + } else if ((ch & 0xe0U) == 0xc0U) { + ch &= 0x1fU; + bytes = 2; + max = 1U << 11; + } else if ((ch & 0xf0U) == 0xe0U) { + ch &= 0x0fU; + bytes = 3; + max = 1U << 16; + } else if ((ch & 0xf8U) == 0xf0U) { + ch &= 0x07U; + bytes = 4; + max = 0x110000U; + } else { + return {bytes, bytes}; // invalid UTF-8 start byte + } + for (size_t i = 1; i < bytes; i++) { + if (i < s.size() && (s[i] & 0xc0) == 0x80) { + ch = (ch << 6) | (s[i] & 0x3f); + } else { + return {i, i}; // invalid UTF-8 encoding; assume one character per byte + } + } + int width = bytes; // in case of overlong encoding + if (ch < max) { + width = widechar_wcwidth(ch); + if (width == widechar_ambiguous) { + width = 1; // just a guess... + } else if (width == widechar_widened_in_9) { + width = 2; + } else if (width < 0) { + width = 0; + } + } + return {width, bytes}; +} + +} namespace nix { @@ -30,7 +77,7 @@ std::string filterANSIEscapes(std::string_view s, bool filterAll, unsigned int w size_t w = 0; auto i = s.begin(); - while (w < (size_t) width && i != s.end()) { + while (i != s.end()) { if (*i == '\e') { std::string e; @@ -48,10 +95,19 @@ std::string filterANSIEscapes(std::string_view s, bool filterAll, unsigned int w } else if (i != s.end() && *i == ']') { // OSC e += *i++; - // eat ESC - while (i != s.end() && *i != '\e') e += *i++; - // eat backslash - if (i != s.end() && *i == '\\') e += last = *i++; + // https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda defines + // two forms of a URI separator: + // 1. ESC '\' (standard) + // 2. BEL ('\a') (xterm-style, used by gcc) + + // eat ESC or BEL + while (i != s.end() && *i != '\e' && *i != '\a') e += *i++; + if (i != s.end()) { + char v = *i; + e += *i++; + // eat backslash after ESC + if (i != s.end() && v == '\e' && *i == '\\') e += last = *i++; + } } else { if (i != s.end() && *i >= 0x40 && *i <= 0x5f) e += *i++; } @@ -61,10 +117,12 @@ std::string filterANSIEscapes(std::string_view s, bool filterAll, unsigned int w } else if (*i == '\t') { - i++; t += ' '; w++; - while (w < (size_t) width && w % 8) { - t += ' '; w++; - } + do { + if (++w > (size_t) width) + return t; + t += ' '; + } while (w % 8); + i++; } else if (*i == '\r' || *i == '\a') @@ -72,35 +130,18 @@ std::string filterANSIEscapes(std::string_view s, bool filterAll, unsigned int w i++; else { - w++; - // Copy one UTF-8 character. - if ((*i & 0xe0) == 0xc0) { - t += *i++; - if (i != s.end() && ((*i & 0xc0) == 0x80)) t += *i++; - } else if ((*i & 0xf0) == 0xe0) { - t += *i++; - if (i != s.end() && ((*i & 0xc0) == 0x80)) { - t += *i++; - if (i != s.end() && ((*i & 0xc0) == 0x80)) t += *i++; - } - } else if ((*i & 0xf8) == 0xf0) { - t += *i++; - if (i != s.end() && ((*i & 0xc0) == 0x80)) { - t += *i++; - if (i != s.end() && ((*i & 0xc0) == 0x80)) { - t += *i++; - if (i != s.end() && ((*i & 0xc0) == 0x80)) t += *i++; - } - } - } else - t += *i++; + auto [chWidth, bytes] = charWidthUTF8Helper({i, s.end()}); + w += chWidth; + if (w > (size_t) width) { + break; + } + t += {i, i + bytes}; + i += bytes; } } - return t; } - ////////////////////////////////////////////////////////////////////// static Sync> windowSize{{0, 0}}; diff --git a/src/libutil/thread-pool.cc b/src/libutil/thread-pool.cc index 0725c192685..8958bc5509a 100644 --- a/src/libutil/thread-pool.cc +++ b/src/libutil/thread-pool.cc @@ -1,6 +1,6 @@ -#include "thread-pool.hh" -#include "signals.hh" -#include "util.hh" +#include "nix/util/thread-pool.hh" +#include "nix/util/signals.hh" +#include "nix/util/util.hh" namespace nix { diff --git a/src/libutil/union-source-accessor.cc b/src/libutil/union-source-accessor.cc new file mode 100644 index 00000000000..9950f604960 --- /dev/null +++ b/src/libutil/union-source-accessor.cc @@ -0,0 +1,82 @@ +#include "nix/util/source-accessor.hh" + +namespace nix { + +struct UnionSourceAccessor : SourceAccessor +{ + std::vector> accessors; + + UnionSourceAccessor(std::vector> _accessors) + : accessors(std::move(_accessors)) + { + displayPrefix.clear(); + } + + std::string readFile(const CanonPath & path) override + { + for (auto & accessor : accessors) { + auto st = accessor->maybeLstat(path); + if (st) + return accessor->readFile(path); + } + throw FileNotFound("path '%s' does not exist", showPath(path)); + } + + std::optional maybeLstat(const CanonPath & path) override + { + for (auto & accessor : accessors) { + auto st = accessor->maybeLstat(path); + if (st) + return st; + } + return std::nullopt; + } + + DirEntries readDirectory(const CanonPath & path) override + { + DirEntries result; + for (auto & accessor : accessors) { + auto st = accessor->maybeLstat(path); + if (!st) + continue; + for (auto & entry : accessor->readDirectory(path)) + // Don't override entries from previous accessors. + result.insert(entry); + } + return result; + } + + std::string readLink(const CanonPath & path) override + { + for (auto & accessor : accessors) { + auto st = accessor->maybeLstat(path); + if (st) + return accessor->readLink(path); + } + throw FileNotFound("path '%s' does not exist", showPath(path)); + } + + std::string showPath(const CanonPath & path) override + { + for (auto & accessor : accessors) + return accessor->showPath(path); + return SourceAccessor::showPath(path); + } + + std::optional getPhysicalPath(const CanonPath & path) override + { + for (auto & accessor : accessors) { + auto p = accessor->getPhysicalPath(path); + if (p) + return p; + } + return std::nullopt; + } +}; + +ref makeUnionSourceAccessor(std::vector> && accessors) +{ + return make_ref(std::move(accessors)); +} + +} diff --git a/src/libutil/unix-domain-socket.cc b/src/libutil/unix-domain-socket.cc index 1707fdb75e1..2422caf14bb 100644 --- a/src/libutil/unix-domain-socket.cc +++ b/src/libutil/unix-domain-socket.cc @@ -1,6 +1,6 @@ -#include "file-system.hh" -#include "unix-domain-socket.hh" -#include "util.hh" +#include "nix/util/file-system.hh" +#include "nix/util/unix-domain-socket.hh" +#include "nix/util/util.hh" #ifdef _WIN32 # include @@ -8,7 +8,7 @@ #else # include # include -# include "processes.hh" +# include "nix/util/processes.hh" #endif #include @@ -29,7 +29,6 @@ AutoCloseFD createUnixDomainSocket() return fdSocket; } - AutoCloseFD createUnixDomainSocket(const Path & path, mode_t mode) { auto fdSocket = nix::createUnixDomainSocket(); @@ -100,7 +99,6 @@ static void bindConnectProcHelper( } } - void bind(Socket fd, const std::string & path) { unlink(path.c_str()); @@ -108,10 +106,16 @@ void bind(Socket fd, const std::string & path) bindConnectProcHelper("bind", ::bind, fd, path); } +void connect(Socket fd, const std::filesystem::path & path) +{ + bindConnectProcHelper("connect", ::connect, fd, path.string()); +} -void connect(Socket fd, const std::string & path) +AutoCloseFD connect(const std::filesystem::path & path) { - bindConnectProcHelper("connect", ::connect, fd, path); + auto fd = createUnixDomainSocket(); + nix::connect(toSocket(fd.get()), path); + return fd; } } diff --git a/src/libutil/unix/environment-variables.cc b/src/libutil/unix/environment-variables.cc index cd7c8f5e566..0e1ed279490 100644 --- a/src/libutil/unix/environment-variables.cc +++ b/src/libutil/unix/environment-variables.cc @@ -1,6 +1,6 @@ #include -#include "environment-variables.hh" +#include "nix/util/environment-variables.hh" namespace nix { diff --git a/src/libutil/unix/file-descriptor.cc b/src/libutil/unix/file-descriptor.cc index ac7c086af80..0051e8aa43c 100644 --- a/src/libutil/unix/file-descriptor.cc +++ b/src/libutil/unix/file-descriptor.cc @@ -1,13 +1,34 @@ -#include "file-system.hh" -#include "signals.hh" -#include "finally.hh" -#include "serialise.hh" +#include "nix/util/file-system.hh" +#include "nix/util/signals.hh" +#include "nix/util/finally.hh" +#include "nix/util/serialise.hh" #include #include +#include + +#include "util-config-private.hh" +#include "util-unix-config-private.hh" namespace nix { +namespace { + +// This function is needed to handle non-blocking reads/writes. This is needed in the buildhook, because +// somehow the json logger file descriptor ends up being non-blocking and breaks remote-building. +// TODO: get rid of buildhook and remove this function again (https://github.com/NixOS/nix/issues/12688) +void pollFD(int fd, int events) +{ + struct pollfd pfd; + pfd.fd = fd; + pfd.events = events; + int ret = poll(&pfd, 1, -1); + if (ret == -1) { + throw SysError("poll on file descriptor failed"); + } +} +} + std::string readFile(int fd) { struct stat st; @@ -17,14 +38,18 @@ std::string readFile(int fd) return drainFD(fd, true, st.st_size); } - void readFull(int fd, char * buf, size_t count) { while (count) { checkInterrupt(); ssize_t res = read(fd, buf, count); if (res == -1) { - if (errno == EINTR) continue; + switch (errno) { + case EINTR: continue; + case EAGAIN: + pollFD(fd, POLLIN); + continue; + } throw SysError("reading from file"); } if (res == 0) throw EndOfFile("unexpected end-of-file"); @@ -39,8 +64,15 @@ void writeFull(int fd, std::string_view s, bool allowInterrupts) while (!s.empty()) { if (allowInterrupts) checkInterrupt(); ssize_t res = write(fd, s.data(), s.size()); - if (res == -1 && errno != EINTR) + if (res == -1) { + switch (errno) { + case EINTR: continue; + case EAGAIN: + pollFD(fd, POLLOUT); + continue; + } throw SysError("writing to file"); + } if (res > 0) s.remove_prefix(res); } @@ -56,8 +88,15 @@ std::string readLine(int fd, bool eofOk) // FIXME: inefficient ssize_t rd = read(fd, &ch, 1); if (rd == -1) { - if (errno != EINTR) + switch (errno) { + case EINTR: continue; + case EAGAIN: { + pollFD(fd, POLLIN); + continue; + } + default: throw SysError("reading a line"); + } } else if (rd == 0) { if (eofOk) return s; @@ -124,7 +163,7 @@ void Pipe::create() ////////////////////////////////////////////////////////////////////// -#if __linux__ || __FreeBSD__ +#if defined(__linux__) || defined(__FreeBSD__) static int unix_close_range(unsigned int first, unsigned int last, int flags) { #if !HAVE_CLOSE_RANGE @@ -140,7 +179,7 @@ void unix::closeExtraFDs() constexpr int MAX_KEPT_FD = 2; static_assert(std::max({STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO}) == MAX_KEPT_FD); -#if __linux__ || __FreeBSD__ +#if defined(__linux__) || defined(__FreeBSD__) // first try to close_range everything we don't care about. if this // returns an error with these parameters we're running on a kernel // that does not implement close_range (i.e. pre 5.9) and fall back @@ -150,9 +189,9 @@ void unix::closeExtraFDs() } #endif -#if __linux__ +#ifdef __linux__ try { - for (auto & s : std::filesystem::directory_iterator{"/proc/self/fd"}) { + for (auto & s : DirectoryIterator{"/proc/self/fd"}) { checkInterrupt(); auto fd = std::stoi(s.path().filename()); if (fd > MAX_KEPT_FD) { @@ -162,7 +201,6 @@ void unix::closeExtraFDs() } return; } catch (SysError &) { - } catch (std::filesystem::filesystem_error &) { } #endif diff --git a/src/libutil/unix/file-path.cc b/src/libutil/unix/file-path.cc index cccee86a1d7..0fb1f468ca3 100644 --- a/src/libutil/unix/file-path.cc +++ b/src/libutil/unix/file-path.cc @@ -3,8 +3,8 @@ #include #include -#include "file-path.hh" -#include "util.hh" +#include "nix/util/file-path.hh" +#include "nix/util/util.hh" namespace nix { diff --git a/src/libutil/unix/file-system.cc b/src/libutil/unix/file-system.cc index bbbbfa5597c..7865de2e9f4 100644 --- a/src/libutil/unix/file-system.cc +++ b/src/libutil/unix/file-system.cc @@ -1,4 +1,16 @@ -#include "file-system.hh" +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "nix/util/file-system.hh" + +#include "util-unix-config-private.hh" namespace nix { @@ -7,4 +19,51 @@ Descriptor openDirectory(const std::filesystem::path & path) return open(path.c_str(), O_RDONLY | O_DIRECTORY); } +void setWriteTime( + const std::filesystem::path & path, time_t accessedTime, time_t modificationTime, std::optional optIsSymlink) +{ + // Would be nice to use std::filesystem unconditionally, but + // doesn't support access time just modification time. + // + // System clock vs File clock issues also make that annoying. +#if HAVE_UTIMENSAT && HAVE_DECL_AT_SYMLINK_NOFOLLOW + struct timespec times[2] = { + { + .tv_sec = accessedTime, + .tv_nsec = 0, + }, + { + .tv_sec = modificationTime, + .tv_nsec = 0, + }, + }; + if (utimensat(AT_FDCWD, path.c_str(), times, AT_SYMLINK_NOFOLLOW) == -1) + throw SysError("changing modification time of %s (using `utimensat`)", path); +#else + struct timeval times[2] = { + { + .tv_sec = accessedTime, + .tv_usec = 0, + }, + { + .tv_sec = modificationTime, + .tv_usec = 0, + }, + }; +# if HAVE_LUTIMES + if (lutimes(path.c_str(), times) == -1) + throw SysError("changing modification time of %s", path); +# else + bool isSymlink = optIsSymlink ? *optIsSymlink : std::filesystem::is_symlink(path); + + if (!isSymlink) { + if (utimes(path.c_str(), times) == -1) + throw SysError("changing modification time of %s (not a symlink)", path); + } else { + throw Error("Cannot change modification time of symlink %s", path); + } +# endif +#endif +} + } diff --git a/src/libutil/unix/include/nix/util/meson.build b/src/libutil/unix/include/nix/util/meson.build new file mode 100644 index 00000000000..b6f1c40d3ad --- /dev/null +++ b/src/libutil/unix/include/nix/util/meson.build @@ -0,0 +1,8 @@ +# Public headers directory + +include_dirs += include_directories('../..') + +headers += files( + 'monitor-fd.hh', + 'signals-impl.hh', +) diff --git a/src/libutil/unix/include/nix/util/monitor-fd.hh b/src/libutil/unix/include/nix/util/monitor-fd.hh new file mode 100644 index 00000000000..c10ad96bd96 --- /dev/null +++ b/src/libutil/unix/include/nix/util/monitor-fd.hh @@ -0,0 +1,130 @@ +#pragma once +///@file + +#include +#include + +#include +#include +#include +#include +#include + +#include "nix/util/signals.hh" + +namespace nix { + +class MonitorFdHup +{ +private: + std::thread thread; + Pipe notifyPipe; + +public: + MonitorFdHup(int fd) + { + notifyPipe.create(); + thread = std::thread([this, fd]() { + while (true) { + // There is a POSIX violation on macOS: you have to listen for + // at least POLLHUP to receive HUP events for a FD. POSIX says + // this is not so, and you should just receive them regardless. + // However, as of our testing on macOS 14.5, the events do not + // get delivered if in the all-bits-unset case, but do get + // delivered if `POLLHUP` is set. + // + // This bug filed as rdar://37537852 + // (https://openradar.appspot.com/37537852). + // + // macOS's own man page + // (https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/poll.2.html) + // additionally says that `POLLHUP` is ignored as an input. It + // seems the likely order of events here was + // + // 1. macOS did not follow the POSIX spec + // + // 2. Somebody ninja-fixed this other spec violation to make + // sure `POLLHUP` was not forgotten about, even though they + // "fixed" this issue in a spec-non-compliant way. Whatever, + // we'll use the fix. + // + // Relevant code, current version, which shows the : + // https://github.com/apple-oss-distributions/xnu/blob/94d3b452840153a99b38a3a9659680b2a006908e/bsd/kern/sys_generic.c#L1751-L1758 + // + // The `POLLHUP` detection was added in + // https://github.com/apple-oss-distributions/xnu/commit/e13b1fa57645afc8a7b2e7d868fe9845c6b08c40#diff-a5aa0b0e7f4d866ca417f60702689fc797e9cdfe33b601b05ccf43086c35d395R1468 + // That means added in 2007 or earlier. Should be good enough + // for us. + short hangup_events = +#ifdef __APPLE__ + POLLHUP +#else + 0 +#endif + ; + + /* Wait indefinitely until a POLLHUP occurs. */ + constexpr size_t num_fds = 2; + struct pollfd fds[num_fds] = { + { + .fd = fd, + .events = hangup_events, + }, + { + .fd = notifyPipe.readSide.get(), + .events = hangup_events, + }, + }; + + auto count = poll(fds, num_fds, -1); + if (count == -1) { + if (errno == EINTR || errno == EAGAIN) + continue; + throw SysError("failed to poll() in MonitorFdHup"); + } + /* This shouldn't happen, but can on macOS due to a bug. + See rdar://37550628. + + This may eventually need a delay or further + coordination with the main thread if spinning proves + too harmful. + */ + if (count == 0) + continue; + if (fds[0].revents & POLLHUP) { + unix::triggerInterrupt(); + break; + } + if (fds[1].revents & POLLHUP) { + break; + } + // On macOS, (jade thinks that) it is possible (although not + // observed on macOS 14.5) that in some limited cases on buggy + // kernel versions, all the non-POLLHUP events for the socket + // get delivered. + // + // We could sleep to avoid pointlessly spinning a thread on + // those, but this opens up a different problem, which is that + // if do sleep, it will be longer before the daemon fork for a + // client exits. Imagine a sequential shell script, running Nix + // commands, each of which talk to the daemon. If the previous + // command registered a temp root, exits, and then the next + // command issues a delete request before the temp root is + // cleaned up, that delete request might fail. + // + // Not sleeping doesn't actually fix the race condition --- we + // would need to block on the old connections' tempt roots being + // cleaned up in in the new connection --- but it does make it + // much less likely. + } + }); + }; + + ~MonitorFdHup() + { + notifyPipe.writeSide.close(); + thread.join(); + } +}; + +} diff --git a/src/libutil/unix/signals-impl.hh b/src/libutil/unix/include/nix/util/signals-impl.hh similarity index 87% rename from src/libutil/unix/signals-impl.hh rename to src/libutil/unix/include/nix/util/signals-impl.hh index 2193922be4c..7397744b2ae 100644 --- a/src/libutil/unix/signals-impl.hh +++ b/src/libutil/unix/include/nix/util/signals-impl.hh @@ -10,10 +10,11 @@ * downstream code.) */ -#include "types.hh" -#include "error.hh" -#include "logging.hh" -#include "ansicolor.hh" +#include "nix/util/types.hh" +#include "nix/util/error.hh" +#include "nix/util/logging.hh" +#include "nix/util/ansicolor.hh" +#include "nix/util/signals.hh" #include #include @@ -84,17 +85,22 @@ static inline bool getInterrupted() return unix::_isInterrupted; } +static inline bool isInterrupted() +{ + using namespace unix; + return _isInterrupted || (interruptCheck && interruptCheck()); +} + /** * Throw `Interrupted` exception if the process has been interrupted. * * Call this in long-running loops and between slow operations to terminate * them as needed. */ -void inline checkInterrupt() +inline void checkInterrupt() { - using namespace unix; - if (_isInterrupted || (interruptCheck && interruptCheck())) - _interrupted(); + if (isInterrupted()) + unix::_interrupted(); } /** diff --git a/src/libutil/unix/meson.build b/src/libutil/unix/meson.build index 1c5bf27fb14..ea2391d0555 100644 --- a/src/libutil/unix/meson.build +++ b/src/libutil/unix/meson.build @@ -1,3 +1,53 @@ +include_dirs += include_directories('.') + +configdata_unix = configuration_data() + +configdata_unix.set( + 'HAVE_DECL_AT_SYMLINK_NOFOLLOW', + cxx.has_header_symbol('fcntl.h', 'AT_SYMLINK_NOFOLLOW').to_int(), + description : 'Optionally used for changing the files and symlinks.' +) + +# Check for each of these functions, and create a define like `#define +# HAVE_CLOSE_RANGE 1`. +check_funcs_unix = [ + [ + 'close_range', + 'For closing many file descriptors after forking.', + ], + [ + 'lutimes', + 'Optionally used for changing the mtime of symlinks.', + ], + [ + 'pipe2', + 'Optionally used for creating pipes on Unix.', + ], + [ + 'strsignal', + 'Optionally used to get more information about processes failing due to a signal on Unix.', + ], + [ + 'sysconf', + 'Optionally used to try to close more file descriptors (e.g. before forking) on Unix.', + ], + [ + 'utimensat', + 'Optionally used for changing the mtime of files and symlinks.', + ], +] +foreach funcspec : check_funcs_unix + define_name = 'HAVE_' + funcspec[0].underscorify().to_upper() + define_value = cxx.has_function(funcspec[0]).to_int() + configdata_unix.set(define_name, define_value, description: funcspec[1]) +endforeach + +config_unix_priv_h = configure_file( + configuration : configdata_unix, + output : 'util-unix-config-private.hh', +) +sources += config_unix_priv_h + sources += files( 'environment-variables.cc', 'file-descriptor.cc', @@ -10,9 +60,4 @@ sources += files( 'users.cc', ) -include_dirs += include_directories('.') - -headers += files( - 'monitor-fd.hh', - 'signals-impl.hh', -) +subdir('include/nix/util') diff --git a/src/libutil/unix/monitor-fd.hh b/src/libutil/unix/monitor-fd.hh deleted file mode 100644 index b6610feff98..00000000000 --- a/src/libutil/unix/monitor-fd.hh +++ /dev/null @@ -1,74 +0,0 @@ -#pragma once -///@file - -#include -#include - -#include -#include -#include -#include -#include - -#include "signals.hh" - -namespace nix { - - -class MonitorFdHup -{ -private: - std::thread thread; - -public: - MonitorFdHup(int fd) - { - thread = std::thread([fd]() { - while (true) { - /* Wait indefinitely until a POLLHUP occurs. */ - struct pollfd fds[1]; - fds[0].fd = fd; - /* Polling for no specific events (i.e. just waiting - for an error/hangup) doesn't work on macOS - anymore. So wait for read events and ignore - them. */ - fds[0].events = - #ifdef __APPLE__ - POLLRDNORM - #else - 0 - #endif - ; - auto count = poll(fds, 1, -1); - if (count == -1) - unreachable(); - - /* This shouldn't happen, but can on macOS due to a bug. - See rdar://37550628. - - This may eventually need a delay or further - coordination with the main thread if spinning proves - too harmful. - */ - if (count == 0) continue; - if (fds[0].revents & POLLHUP) { - unix::triggerInterrupt(); - break; - } - /* This will only happen on macOS. We sleep a bit to - avoid waking up too often if the client is sending - input. */ - sleep(1); - } - }); - }; - - ~MonitorFdHup() - { - pthread_cancel(thread.native_handle()); - thread.join(); - } -}; - - -} diff --git a/src/libutil/unix/muxable-pipe.cc b/src/libutil/unix/muxable-pipe.cc index 0104663c3bf..57bcdb0ad50 100644 --- a/src/libutil/unix/muxable-pipe.cc +++ b/src/libutil/unix/muxable-pipe.cc @@ -1,8 +1,8 @@ #include -#include "logging.hh" -#include "util.hh" -#include "muxable-pipe.hh" +#include "nix/util/logging.hh" +#include "nix/util/util.hh" +#include "nix/util/muxable-pipe.hh" namespace nix { diff --git a/src/libutil/unix/os-string.cc b/src/libutil/unix/os-string.cc index 8378afde292..1a2be1554e3 100644 --- a/src/libutil/unix/os-string.cc +++ b/src/libutil/unix/os-string.cc @@ -3,8 +3,8 @@ #include #include -#include "file-path.hh" -#include "util.hh" +#include "nix/util/file-path.hh" +#include "nix/util/util.hh" namespace nix { diff --git a/src/libutil/unix/processes.cc b/src/libutil/unix/processes.cc index 43d9179d9d7..0d50fc303e1 100644 --- a/src/libutil/unix/processes.cc +++ b/src/libutil/unix/processes.cc @@ -1,10 +1,10 @@ -#include "current-process.hh" -#include "environment-variables.hh" -#include "executable-path.hh" -#include "signals.hh" -#include "processes.hh" -#include "finally.hh" -#include "serialise.hh" +#include "nix/util/current-process.hh" +#include "nix/util/environment-variables.hh" +#include "nix/util/executable-path.hh" +#include "nix/util/signals.hh" +#include "nix/util/processes.hh" +#include "nix/util/finally.hh" +#include "nix/util/serialise.hh" #include #include @@ -28,6 +28,9 @@ # include #endif +#include "util-config-private.hh" +#include "util-unix-config-private.hh" + namespace nix { @@ -75,7 +78,7 @@ int Pid::kill() /* On BSDs, killing a process group will return EPERM if all processes in the group are zombies (or something like that). So try to detect and ignore that situation. */ -#if __FreeBSD__ || __APPLE__ +#if defined(__FreeBSD__) || defined(__APPLE__) if (errno != EPERM || ::kill(pid, 0) != 0) #endif logError(SysError("killing process %d", pid).info()); @@ -187,7 +190,7 @@ static pid_t doFork(bool allowVfork, ChildWrapperFunction & fun) } -#if __linux__ +#ifdef __linux__ static int childEntry(void * arg) { auto & fun = *reinterpret_cast(arg); @@ -199,11 +202,19 @@ static int childEntry(void * arg) pid_t startProcess(std::function fun, const ProcessOptions & options) { + auto newLogger = makeSimpleLogger(); ChildWrapperFunction wrapper = [&] { - if (!options.allowVfork) - logger = makeSimpleLogger(); + if (!options.allowVfork) { + /* Set a simple logger, while releasing (not destroying) + the parent logger. We don't want to run the parent + logger's destructor since that will crash (e.g. when + ~ProgressBar() tries to join a thread that doesn't + exist. */ + logger.release(); + logger = std::move(newLogger); + } try { -#if __linux__ +#ifdef __linux__ if (options.dieWithParent && prctl(PR_SET_PDEATHSIG, SIGKILL) == -1) throw SysError("setting death signal"); #endif @@ -299,15 +310,7 @@ void runProgram2(const RunOptions & options) // case), so we can't use it if we alter the environment processOptions.allowVfork = !options.environment; - std::optional>> resumeLoggerDefer; - if (options.isInteractive) { - logger->pause(); - resumeLoggerDefer.emplace( - []() { - logger->resume(); - } - ); - } + auto suspension = logger->suspendIf(options.isInteractive); /* Fork. */ Pid pid = startProcess([&] { diff --git a/src/libutil/unix/signals.cc b/src/libutil/unix/signals.cc index d0608dace67..665b9b096e1 100644 --- a/src/libutil/unix/signals.cc +++ b/src/libutil/unix/signals.cc @@ -1,8 +1,8 @@ -#include "signals.hh" -#include "util.hh" -#include "error.hh" -#include "sync.hh" -#include "terminal.hh" +#include "nix/util/signals.hh" +#include "nix/util/util.hh" +#include "nix/util/error.hh" +#include "nix/util/sync.hh" +#include "nix/util/terminal.hh" #include @@ -105,7 +105,7 @@ void unix::setChildSignalMask(sigset_t * sigs) { assert(sigs); // C style function, but think of sigs as a reference -#if _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE +#if (defined(_POSIX_C_SOURCE) && _POSIX_C_SOURCE >= 1) || (defined(_XOPEN_SOURCE) && _XOPEN_SOURCE) || (defined(_POSIX_SOURCE) && _POSIX_SOURCE) sigemptyset(&savedSignalMask); // There's no "assign" or "copy" function, so we rely on (math) idempotence // of the or operator: a or a = a. diff --git a/src/libutil/unix/users.cc b/src/libutil/unix/users.cc index 107a6e04f98..5ac851e9551 100644 --- a/src/libutil/unix/users.cc +++ b/src/libutil/unix/users.cc @@ -1,7 +1,7 @@ -#include "util.hh" -#include "users.hh" -#include "environment-variables.hh" -#include "file-system.hh" +#include "nix/util/util.hh" +#include "nix/util/users.hh" +#include "nix/util/environment-variables.hh" +#include "nix/util/file-system.hh" #include #include @@ -9,8 +9,6 @@ namespace nix { -namespace fs { using namespace std::filesystem; } - std::string getUserName() { auto pw = getpwuid(geteuid()); diff --git a/src/libutil/url.cc b/src/libutil/url.cc index 63b9734ee51..b7286072dac 100644 --- a/src/libutil/url.cc +++ b/src/libutil/url.cc @@ -1,8 +1,8 @@ -#include "url.hh" -#include "url-parts.hh" -#include "util.hh" -#include "split.hh" -#include "canon-path.hh" +#include "nix/util/url.hh" +#include "nix/util/url-parts.hh" +#include "nix/util/util.hh" +#include "nix/util/split.hh" +#include "nix/util/canon-path.hh" namespace nix { @@ -22,7 +22,6 @@ ParsedURL parseURL(const std::string & url) std::smatch match; if (std::regex_match(url, match, uriRegex)) { - auto & base = match[1]; std::string scheme = match[2]; auto authority = match[3].matched ? std::optional(match[3]) : std::nullopt; @@ -40,8 +39,6 @@ ParsedURL parseURL(const std::string & url) path = "/"; return ParsedURL{ - .url = url, - .base = base, .scheme = scheme, .authority = authority, .path = percentDecode(path), @@ -73,9 +70,9 @@ std::string percentDecode(std::string_view in) return decoded; } -std::map decodeQuery(const std::string & query) +StringMap decodeQuery(const std::string & query) { - std::map result; + StringMap result; for (const auto & s : tokenizeString(query, "&")) { auto e = s.find('='); @@ -111,7 +108,7 @@ std::string percentEncode(std::string_view s, std::string_view keep) return res; } -std::string encodeQuery(const std::map & ss) +std::string encodeQuery(const StringMap & ss) { std::string res; bool first = true; @@ -136,6 +133,12 @@ std::string ParsedURL::to_string() const + (fragment.empty() ? "" : "#" + percentEncode(fragment)); } +std::ostream & operator << (std::ostream & os, const ParsedURL & url) +{ + os << url.to_string(); + return os; +} + bool ParsedURL::operator ==(const ParsedURL & other) const noexcept { return diff --git a/src/libutil/users.cc b/src/libutil/users.cc index b4bc67cbcf2..5a5d740c687 100644 --- a/src/libutil/users.cc +++ b/src/libutil/users.cc @@ -1,7 +1,7 @@ -#include "util.hh" -#include "users.hh" -#include "environment-variables.hh" -#include "file-system.hh" +#include "nix/util/util.hh" +#include "nix/util/users.hh" +#include "nix/util/environment-variables.hh" +#include "nix/util/file-system.hh" namespace nix { diff --git a/src/libutil/util.cc b/src/libutil/util.cc index ed5c7e4f1ef..c9cc80fef6c 100644 --- a/src/libutil/util.cc +++ b/src/libutil/util.cc @@ -1,7 +1,7 @@ -#include "util.hh" -#include "fmt.hh" -#include "file-path.hh" -#include "signals.hh" +#include "nix/util/util.hh" +#include "nix/util/fmt.hh" +#include "nix/util/file-path.hh" +#include "nix/util/signals.hh" #include #include @@ -171,7 +171,7 @@ std::string toLower(std::string s) } -std::string shellEscape(const std::string_view s) +std::string escapeShellArgAlways(const std::string_view s) { std::string r; r.reserve(s.size() + 2); diff --git a/src/libutil/widecharwidth/LICENSE b/src/libutil/widecharwidth/LICENSE new file mode 100644 index 00000000000..d3b1dd767e2 --- /dev/null +++ b/src/libutil/widecharwidth/LICENSE @@ -0,0 +1,4 @@ +widecharwidth - wcwidth implementation +Written in 2018 by ridiculous_fish +To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty. +You should have received a copy of the CC0 Public Domain Dedication along with this software. If not, see . diff --git a/src/libutil/widecharwidth/widechar_width.h b/src/libutil/widecharwidth/widechar_width.h new file mode 100644 index 00000000000..92e63e91347 --- /dev/null +++ b/src/libutil/widecharwidth/widechar_width.h @@ -0,0 +1,1559 @@ +/** + * widechar_width.h for Unicode 16.0.0 + * See https://github.com/ridiculousfish/widecharwidth/ + * + * SHA1 file hashes: + * ( + * the hashes for generate.py and the template are git object hashes, + * use `git log --all --find-object=` in the widecharwidth repository + * to see which commit they correspond to, + * or run `git hash-object` on the file to compare. + * The other hashes are simple `sha1sum` style hashes. + * ) + * + * generate.py: 2747bb9402d8eeeca8e566ff947f14308511ecb1 + * template.js: 1249763c5b7c1e308aeb4ca64f1e15bce1fab9b3 + * UnicodeData.txt: 91df83276154240bcedef82a09bde77aa182cf8d + * EastAsianWidth.txt: 0885c0fc1c21eb58954a3bfb785d78559b361d92 + * emoji-data.txt: 1df2f8329dd9f5c238674807de736f316c6b9d87 + */ + +#ifndef WIDECHAR_WIDTH_H +#define WIDECHAR_WIDTH_H + +#include +#include +#include +#include + +namespace { + +/* Special width values */ +enum { + widechar_nonprint = -1, // The character is not printable. + widechar_combining = -2, // The character is a zero-width combiner. + widechar_ambiguous = -3, // The character is East-Asian ambiguous width. + widechar_private_use = -4, // The character is for private use. + widechar_unassigned = -5, // The character is unassigned. + widechar_widened_in_9 = -6, // Width is 1 in Unicode 8, 2 in Unicode 9+. + widechar_non_character = -7 // The character is a noncharacter. +}; + +/* An inclusive range of characters. */ +struct widechar_range { + uint32_t lo; + uint32_t hi; +}; + +/* Simple ASCII characters - used a lot, so we check them first. */ +static const struct widechar_range widechar_ascii_table[] = { + {0x00020, 0x0007E} +}; + +/* Private usage range. */ +static const struct widechar_range widechar_private_table[] = { + {0x0E000, 0x0F8FF}, + {0xF0000, 0xFFFFD}, + {0x100000, 0x10FFFD} +}; + +/* Nonprinting characters. */ +static const struct widechar_range widechar_nonprint_table[] = { + {0x00000, 0x0001F}, + {0x0007F, 0x0009F}, + {0x000AD, 0x000AD}, + {0x00600, 0x00605}, + {0x0061C, 0x0061C}, + {0x006DD, 0x006DD}, + {0x0070F, 0x0070F}, + {0x00890, 0x00891}, + {0x008E2, 0x008E2}, + {0x0180E, 0x0180E}, + {0x0200B, 0x0200F}, + {0x02028, 0x0202E}, + {0x02060, 0x02064}, + {0x02066, 0x0206F}, + {0x0D800, 0x0DFFF}, + {0x0FEFF, 0x0FEFF}, + {0x0FFF9, 0x0FFFB}, + {0x110BD, 0x110BD}, + {0x110CD, 0x110CD}, + {0x13430, 0x1343F}, + {0x1BCA0, 0x1BCA3}, + {0x1D173, 0x1D17A}, + {0xE0001, 0xE0001}, + {0xE0020, 0xE007F} +}; + +/* Width 0 combining marks. */ +static const struct widechar_range widechar_combining_table[] = { + {0x00300, 0x0036F}, + {0x00483, 0x00489}, + {0x00591, 0x005BD}, + {0x005BF, 0x005BF}, + {0x005C1, 0x005C2}, + {0x005C4, 0x005C5}, + {0x005C7, 0x005C7}, + {0x00610, 0x0061A}, + {0x0064B, 0x0065F}, + {0x00670, 0x00670}, + {0x006D6, 0x006DC}, + {0x006DF, 0x006E4}, + {0x006E7, 0x006E8}, + {0x006EA, 0x006ED}, + {0x00711, 0x00711}, + {0x00730, 0x0074A}, + {0x007A6, 0x007B0}, + {0x007EB, 0x007F3}, + {0x007FD, 0x007FD}, + {0x00816, 0x00819}, + {0x0081B, 0x00823}, + {0x00825, 0x00827}, + {0x00829, 0x0082D}, + {0x00859, 0x0085B}, + {0x00897, 0x0089F}, + {0x008CA, 0x008E1}, + {0x008E3, 0x00903}, + {0x0093A, 0x0093C}, + {0x0093E, 0x0094F}, + {0x00951, 0x00957}, + {0x00962, 0x00963}, + {0x00981, 0x00983}, + {0x009BC, 0x009BC}, + {0x009BE, 0x009C4}, + {0x009C7, 0x009C8}, + {0x009CB, 0x009CD}, + {0x009D7, 0x009D7}, + {0x009E2, 0x009E3}, + {0x009FE, 0x009FE}, + {0x00A01, 0x00A03}, + {0x00A3C, 0x00A3C}, + {0x00A3E, 0x00A42}, + {0x00A47, 0x00A48}, + {0x00A4B, 0x00A4D}, + {0x00A51, 0x00A51}, + {0x00A70, 0x00A71}, + {0x00A75, 0x00A75}, + {0x00A81, 0x00A83}, + {0x00ABC, 0x00ABC}, + {0x00ABE, 0x00AC5}, + {0x00AC7, 0x00AC9}, + {0x00ACB, 0x00ACD}, + {0x00AE2, 0x00AE3}, + {0x00AFA, 0x00AFF}, + {0x00B01, 0x00B03}, + {0x00B3C, 0x00B3C}, + {0x00B3E, 0x00B44}, + {0x00B47, 0x00B48}, + {0x00B4B, 0x00B4D}, + {0x00B55, 0x00B57}, + {0x00B62, 0x00B63}, + {0x00B82, 0x00B82}, + {0x00BBE, 0x00BC2}, + {0x00BC6, 0x00BC8}, + {0x00BCA, 0x00BCD}, + {0x00BD7, 0x00BD7}, + {0x00C00, 0x00C04}, + {0x00C3C, 0x00C3C}, + {0x00C3E, 0x00C44}, + {0x00C46, 0x00C48}, + {0x00C4A, 0x00C4D}, + {0x00C55, 0x00C56}, + {0x00C62, 0x00C63}, + {0x00C81, 0x00C83}, + {0x00CBC, 0x00CBC}, + {0x00CBE, 0x00CC4}, + {0x00CC6, 0x00CC8}, + {0x00CCA, 0x00CCD}, + {0x00CD5, 0x00CD6}, + {0x00CE2, 0x00CE3}, + {0x00CF3, 0x00CF3}, + {0x00D00, 0x00D03}, + {0x00D3B, 0x00D3C}, + {0x00D3E, 0x00D44}, + {0x00D46, 0x00D48}, + {0x00D4A, 0x00D4D}, + {0x00D57, 0x00D57}, + {0x00D62, 0x00D63}, + {0x00D81, 0x00D83}, + {0x00DCA, 0x00DCA}, + {0x00DCF, 0x00DD4}, + {0x00DD6, 0x00DD6}, + {0x00DD8, 0x00DDF}, + {0x00DF2, 0x00DF3}, + {0x00E31, 0x00E31}, + {0x00E34, 0x00E3A}, + {0x00E47, 0x00E4E}, + {0x00EB1, 0x00EB1}, + {0x00EB4, 0x00EBC}, + {0x00EC8, 0x00ECE}, + {0x00F18, 0x00F19}, + {0x00F35, 0x00F35}, + {0x00F37, 0x00F37}, + {0x00F39, 0x00F39}, + {0x00F3E, 0x00F3F}, + {0x00F71, 0x00F84}, + {0x00F86, 0x00F87}, + {0x00F8D, 0x00F97}, + {0x00F99, 0x00FBC}, + {0x00FC6, 0x00FC6}, + {0x0102B, 0x0103E}, + {0x01056, 0x01059}, + {0x0105E, 0x01060}, + {0x01062, 0x01064}, + {0x01067, 0x0106D}, + {0x01071, 0x01074}, + {0x01082, 0x0108D}, + {0x0108F, 0x0108F}, + {0x0109A, 0x0109D}, + {0x0135D, 0x0135F}, + {0x01712, 0x01715}, + {0x01732, 0x01734}, + {0x01752, 0x01753}, + {0x01772, 0x01773}, + {0x017B4, 0x017D3}, + {0x017DD, 0x017DD}, + {0x0180B, 0x0180D}, + {0x0180F, 0x0180F}, + {0x01885, 0x01886}, + {0x018A9, 0x018A9}, + {0x01920, 0x0192B}, + {0x01930, 0x0193B}, + {0x01A17, 0x01A1B}, + {0x01A55, 0x01A5E}, + {0x01A60, 0x01A7C}, + {0x01A7F, 0x01A7F}, + {0x01AB0, 0x01ACE}, + {0x01B00, 0x01B04}, + {0x01B34, 0x01B44}, + {0x01B6B, 0x01B73}, + {0x01B80, 0x01B82}, + {0x01BA1, 0x01BAD}, + {0x01BE6, 0x01BF3}, + {0x01C24, 0x01C37}, + {0x01CD0, 0x01CD2}, + {0x01CD4, 0x01CE8}, + {0x01CED, 0x01CED}, + {0x01CF4, 0x01CF4}, + {0x01CF7, 0x01CF9}, + {0x01DC0, 0x01DFF}, + {0x020D0, 0x020F0}, + {0x02CEF, 0x02CF1}, + {0x02D7F, 0x02D7F}, + {0x02DE0, 0x02DFF}, + {0x0302A, 0x0302F}, + {0x03099, 0x0309A}, + {0x0A66F, 0x0A672}, + {0x0A674, 0x0A67D}, + {0x0A69E, 0x0A69F}, + {0x0A6F0, 0x0A6F1}, + {0x0A802, 0x0A802}, + {0x0A806, 0x0A806}, + {0x0A80B, 0x0A80B}, + {0x0A823, 0x0A827}, + {0x0A82C, 0x0A82C}, + {0x0A880, 0x0A881}, + {0x0A8B4, 0x0A8C5}, + {0x0A8E0, 0x0A8F1}, + {0x0A8FF, 0x0A8FF}, + {0x0A926, 0x0A92D}, + {0x0A947, 0x0A953}, + {0x0A980, 0x0A983}, + {0x0A9B3, 0x0A9C0}, + {0x0A9E5, 0x0A9E5}, + {0x0AA29, 0x0AA36}, + {0x0AA43, 0x0AA43}, + {0x0AA4C, 0x0AA4D}, + {0x0AA7B, 0x0AA7D}, + {0x0AAB0, 0x0AAB0}, + {0x0AAB2, 0x0AAB4}, + {0x0AAB7, 0x0AAB8}, + {0x0AABE, 0x0AABF}, + {0x0AAC1, 0x0AAC1}, + {0x0AAEB, 0x0AAEF}, + {0x0AAF5, 0x0AAF6}, + {0x0ABE3, 0x0ABEA}, + {0x0ABEC, 0x0ABED}, + {0x0FB1E, 0x0FB1E}, + {0x0FE00, 0x0FE0F}, + {0x0FE20, 0x0FE2F}, + {0x101FD, 0x101FD}, + {0x102E0, 0x102E0}, + {0x10376, 0x1037A}, + {0x10A01, 0x10A03}, + {0x10A05, 0x10A06}, + {0x10A0C, 0x10A0F}, + {0x10A38, 0x10A3A}, + {0x10A3F, 0x10A3F}, + {0x10AE5, 0x10AE6}, + {0x10D24, 0x10D27}, + {0x10D69, 0x10D6D}, + {0x10EAB, 0x10EAC}, + {0x10EFC, 0x10EFF}, + {0x10F46, 0x10F50}, + {0x10F82, 0x10F85}, + {0x11000, 0x11002}, + {0x11038, 0x11046}, + {0x11070, 0x11070}, + {0x11073, 0x11074}, + {0x1107F, 0x11082}, + {0x110B0, 0x110BA}, + {0x110C2, 0x110C2}, + {0x11100, 0x11102}, + {0x11127, 0x11134}, + {0x11145, 0x11146}, + {0x11173, 0x11173}, + {0x11180, 0x11182}, + {0x111B3, 0x111C0}, + {0x111C9, 0x111CC}, + {0x111CE, 0x111CF}, + {0x1122C, 0x11237}, + {0x1123E, 0x1123E}, + {0x11241, 0x11241}, + {0x112DF, 0x112EA}, + {0x11300, 0x11303}, + {0x1133B, 0x1133C}, + {0x1133E, 0x11344}, + {0x11347, 0x11348}, + {0x1134B, 0x1134D}, + {0x11357, 0x11357}, + {0x11362, 0x11363}, + {0x11366, 0x1136C}, + {0x11370, 0x11374}, + {0x113B8, 0x113C0}, + {0x113C2, 0x113C2}, + {0x113C5, 0x113C5}, + {0x113C7, 0x113CA}, + {0x113CC, 0x113D0}, + {0x113D2, 0x113D2}, + {0x113E1, 0x113E2}, + {0x11435, 0x11446}, + {0x1145E, 0x1145E}, + {0x114B0, 0x114C3}, + {0x115AF, 0x115B5}, + {0x115B8, 0x115C0}, + {0x115DC, 0x115DD}, + {0x11630, 0x11640}, + {0x116AB, 0x116B7}, + {0x1171D, 0x1172B}, + {0x1182C, 0x1183A}, + {0x11930, 0x11935}, + {0x11937, 0x11938}, + {0x1193B, 0x1193E}, + {0x11940, 0x11940}, + {0x11942, 0x11943}, + {0x119D1, 0x119D7}, + {0x119DA, 0x119E0}, + {0x119E4, 0x119E4}, + {0x11A01, 0x11A0A}, + {0x11A33, 0x11A39}, + {0x11A3B, 0x11A3E}, + {0x11A47, 0x11A47}, + {0x11A51, 0x11A5B}, + {0x11A8A, 0x11A99}, + {0x11C2F, 0x11C36}, + {0x11C38, 0x11C3F}, + {0x11C92, 0x11CA7}, + {0x11CA9, 0x11CB6}, + {0x11D31, 0x11D36}, + {0x11D3A, 0x11D3A}, + {0x11D3C, 0x11D3D}, + {0x11D3F, 0x11D45}, + {0x11D47, 0x11D47}, + {0x11D8A, 0x11D8E}, + {0x11D90, 0x11D91}, + {0x11D93, 0x11D97}, + {0x11EF3, 0x11EF6}, + {0x11F00, 0x11F01}, + {0x11F03, 0x11F03}, + {0x11F34, 0x11F3A}, + {0x11F3E, 0x11F42}, + {0x11F5A, 0x11F5A}, + {0x13440, 0x13440}, + {0x13447, 0x13455}, + {0x1611E, 0x1612F}, + {0x16AF0, 0x16AF4}, + {0x16B30, 0x16B36}, + {0x16F4F, 0x16F4F}, + {0x16F51, 0x16F87}, + {0x16F8F, 0x16F92}, + {0x16FE4, 0x16FE4}, + {0x16FF0, 0x16FF1}, + {0x1BC9D, 0x1BC9E}, + {0x1CF00, 0x1CF2D}, + {0x1CF30, 0x1CF46}, + {0x1D165, 0x1D169}, + {0x1D16D, 0x1D172}, + {0x1D17B, 0x1D182}, + {0x1D185, 0x1D18B}, + {0x1D1AA, 0x1D1AD}, + {0x1D242, 0x1D244}, + {0x1DA00, 0x1DA36}, + {0x1DA3B, 0x1DA6C}, + {0x1DA75, 0x1DA75}, + {0x1DA84, 0x1DA84}, + {0x1DA9B, 0x1DA9F}, + {0x1DAA1, 0x1DAAF}, + {0x1E000, 0x1E006}, + {0x1E008, 0x1E018}, + {0x1E01B, 0x1E021}, + {0x1E023, 0x1E024}, + {0x1E026, 0x1E02A}, + {0x1E08F, 0x1E08F}, + {0x1E130, 0x1E136}, + {0x1E2AE, 0x1E2AE}, + {0x1E2EC, 0x1E2EF}, + {0x1E4EC, 0x1E4EF}, + {0x1E5EE, 0x1E5EF}, + {0x1E8D0, 0x1E8D6}, + {0x1E944, 0x1E94A}, + {0xE0100, 0xE01EF} +}; + +/* Width 0 combining letters. */ +static const struct widechar_range widechar_combiningletters_table[] = { + {0x01160, 0x011FF}, + {0x0D7B0, 0x0D7FF} +}; + +/* Width 2 characters. */ +static const struct widechar_range widechar_doublewide_table[] = { + {0x01100, 0x0115F}, + {0x02329, 0x0232A}, + {0x02630, 0x02637}, + {0x0268A, 0x0268F}, + {0x02E80, 0x02E99}, + {0x02E9B, 0x02EF3}, + {0x02F00, 0x02FD5}, + {0x02FF0, 0x0303E}, + {0x03041, 0x03096}, + {0x03099, 0x030FF}, + {0x03105, 0x0312F}, + {0x03131, 0x0318E}, + {0x03190, 0x031E5}, + {0x031EF, 0x0321E}, + {0x03220, 0x03247}, + {0x03250, 0x0A48C}, + {0x0A490, 0x0A4C6}, + {0x0A960, 0x0A97C}, + {0x0AC00, 0x0D7A3}, + {0x0F900, 0x0FAFF}, + {0x0FE10, 0x0FE19}, + {0x0FE30, 0x0FE52}, + {0x0FE54, 0x0FE66}, + {0x0FE68, 0x0FE6B}, + {0x0FF01, 0x0FF60}, + {0x0FFE0, 0x0FFE6}, + {0x16FE0, 0x16FE4}, + {0x16FF0, 0x16FF1}, + {0x17000, 0x187F7}, + {0x18800, 0x18CD5}, + {0x18CFF, 0x18D08}, + {0x1AFF0, 0x1AFF3}, + {0x1AFF5, 0x1AFFB}, + {0x1AFFD, 0x1AFFE}, + {0x1B000, 0x1B122}, + {0x1B132, 0x1B132}, + {0x1B150, 0x1B152}, + {0x1B155, 0x1B155}, + {0x1B164, 0x1B167}, + {0x1B170, 0x1B2FB}, + {0x1D300, 0x1D356}, + {0x1D360, 0x1D376}, + {0x1F200, 0x1F200}, + {0x1F202, 0x1F202}, + {0x1F210, 0x1F219}, + {0x1F21B, 0x1F22E}, + {0x1F230, 0x1F231}, + {0x1F237, 0x1F237}, + {0x1F23B, 0x1F23B}, + {0x1F240, 0x1F248}, + {0x1F260, 0x1F265}, + {0x1F57A, 0x1F57A}, + {0x1F5A4, 0x1F5A4}, + {0x1F6D1, 0x1F6D2}, + {0x1F6D5, 0x1F6D7}, + {0x1F6DC, 0x1F6DF}, + {0x1F6F4, 0x1F6FC}, + {0x1F7E0, 0x1F7EB}, + {0x1F7F0, 0x1F7F0}, + {0x1F90C, 0x1F90F}, + {0x1F919, 0x1F93A}, + {0x1F93C, 0x1F945}, + {0x1F947, 0x1F97F}, + {0x1F985, 0x1F9BF}, + {0x1F9C1, 0x1F9FF}, + {0x1FA70, 0x1FA7C}, + {0x1FA80, 0x1FA89}, + {0x1FA8F, 0x1FAC6}, + {0x1FACE, 0x1FADC}, + {0x1FADF, 0x1FAE9}, + {0x1FAF0, 0x1FAF8}, + {0x20000, 0x2FFFD}, + {0x30000, 0x3FFFD} +}; + +/* Ambiguous-width characters. */ +static const struct widechar_range widechar_ambiguous_table[] = { + {0x000A1, 0x000A1}, + {0x000A4, 0x000A4}, + {0x000A7, 0x000A8}, + {0x000AA, 0x000AA}, + {0x000AD, 0x000AE}, + {0x000B0, 0x000B4}, + {0x000B6, 0x000BA}, + {0x000BC, 0x000BF}, + {0x000C6, 0x000C6}, + {0x000D0, 0x000D0}, + {0x000D7, 0x000D8}, + {0x000DE, 0x000E1}, + {0x000E6, 0x000E6}, + {0x000E8, 0x000EA}, + {0x000EC, 0x000ED}, + {0x000F0, 0x000F0}, + {0x000F2, 0x000F3}, + {0x000F7, 0x000FA}, + {0x000FC, 0x000FC}, + {0x000FE, 0x000FE}, + {0x00101, 0x00101}, + {0x00111, 0x00111}, + {0x00113, 0x00113}, + {0x0011B, 0x0011B}, + {0x00126, 0x00127}, + {0x0012B, 0x0012B}, + {0x00131, 0x00133}, + {0x00138, 0x00138}, + {0x0013F, 0x00142}, + {0x00144, 0x00144}, + {0x00148, 0x0014B}, + {0x0014D, 0x0014D}, + {0x00152, 0x00153}, + {0x00166, 0x00167}, + {0x0016B, 0x0016B}, + {0x001CE, 0x001CE}, + {0x001D0, 0x001D0}, + {0x001D2, 0x001D2}, + {0x001D4, 0x001D4}, + {0x001D6, 0x001D6}, + {0x001D8, 0x001D8}, + {0x001DA, 0x001DA}, + {0x001DC, 0x001DC}, + {0x00251, 0x00251}, + {0x00261, 0x00261}, + {0x002C4, 0x002C4}, + {0x002C7, 0x002C7}, + {0x002C9, 0x002CB}, + {0x002CD, 0x002CD}, + {0x002D0, 0x002D0}, + {0x002D8, 0x002DB}, + {0x002DD, 0x002DD}, + {0x002DF, 0x002DF}, + {0x00300, 0x0036F}, + {0x00391, 0x003A1}, + {0x003A3, 0x003A9}, + {0x003B1, 0x003C1}, + {0x003C3, 0x003C9}, + {0x00401, 0x00401}, + {0x00410, 0x0044F}, + {0x00451, 0x00451}, + {0x02010, 0x02010}, + {0x02013, 0x02016}, + {0x02018, 0x02019}, + {0x0201C, 0x0201D}, + {0x02020, 0x02022}, + {0x02024, 0x02027}, + {0x02030, 0x02030}, + {0x02032, 0x02033}, + {0x02035, 0x02035}, + {0x0203B, 0x0203B}, + {0x0203E, 0x0203E}, + {0x02074, 0x02074}, + {0x0207F, 0x0207F}, + {0x02081, 0x02084}, + {0x020AC, 0x020AC}, + {0x02103, 0x02103}, + {0x02105, 0x02105}, + {0x02109, 0x02109}, + {0x02113, 0x02113}, + {0x02116, 0x02116}, + {0x02121, 0x02122}, + {0x02126, 0x02126}, + {0x0212B, 0x0212B}, + {0x02153, 0x02154}, + {0x0215B, 0x0215E}, + {0x02160, 0x0216B}, + {0x02170, 0x02179}, + {0x02189, 0x02189}, + {0x02190, 0x02199}, + {0x021B8, 0x021B9}, + {0x021D2, 0x021D2}, + {0x021D4, 0x021D4}, + {0x021E7, 0x021E7}, + {0x02200, 0x02200}, + {0x02202, 0x02203}, + {0x02207, 0x02208}, + {0x0220B, 0x0220B}, + {0x0220F, 0x0220F}, + {0x02211, 0x02211}, + {0x02215, 0x02215}, + {0x0221A, 0x0221A}, + {0x0221D, 0x02220}, + {0x02223, 0x02223}, + {0x02225, 0x02225}, + {0x02227, 0x0222C}, + {0x0222E, 0x0222E}, + {0x02234, 0x02237}, + {0x0223C, 0x0223D}, + {0x02248, 0x02248}, + {0x0224C, 0x0224C}, + {0x02252, 0x02252}, + {0x02260, 0x02261}, + {0x02264, 0x02267}, + {0x0226A, 0x0226B}, + {0x0226E, 0x0226F}, + {0x02282, 0x02283}, + {0x02286, 0x02287}, + {0x02295, 0x02295}, + {0x02299, 0x02299}, + {0x022A5, 0x022A5}, + {0x022BF, 0x022BF}, + {0x02312, 0x02312}, + {0x02460, 0x024E9}, + {0x024EB, 0x0254B}, + {0x02550, 0x02573}, + {0x02580, 0x0258F}, + {0x02592, 0x02595}, + {0x025A0, 0x025A1}, + {0x025A3, 0x025A9}, + {0x025B2, 0x025B3}, + {0x025B6, 0x025B7}, + {0x025BC, 0x025BD}, + {0x025C0, 0x025C1}, + {0x025C6, 0x025C8}, + {0x025CB, 0x025CB}, + {0x025CE, 0x025D1}, + {0x025E2, 0x025E5}, + {0x025EF, 0x025EF}, + {0x02605, 0x02606}, + {0x02609, 0x02609}, + {0x0260E, 0x0260F}, + {0x0261C, 0x0261C}, + {0x0261E, 0x0261E}, + {0x02640, 0x02640}, + {0x02642, 0x02642}, + {0x02660, 0x02661}, + {0x02663, 0x02665}, + {0x02667, 0x0266A}, + {0x0266C, 0x0266D}, + {0x0266F, 0x0266F}, + {0x0269E, 0x0269F}, + {0x026BF, 0x026BF}, + {0x026C6, 0x026CD}, + {0x026CF, 0x026D3}, + {0x026D5, 0x026E1}, + {0x026E3, 0x026E3}, + {0x026E8, 0x026E9}, + {0x026EB, 0x026F1}, + {0x026F4, 0x026F4}, + {0x026F6, 0x026F9}, + {0x026FB, 0x026FC}, + {0x026FE, 0x026FF}, + {0x0273D, 0x0273D}, + {0x02776, 0x0277F}, + {0x02B56, 0x02B59}, + {0x03248, 0x0324F}, + {0x0E000, 0x0F8FF}, + {0x0FE00, 0x0FE0F}, + {0x0FFFD, 0x0FFFD}, + {0x1F100, 0x1F10A}, + {0x1F110, 0x1F12D}, + {0x1F130, 0x1F169}, + {0x1F170, 0x1F18D}, + {0x1F18F, 0x1F190}, + {0x1F19B, 0x1F1AC}, + {0xE0100, 0xE01EF}, + {0xF0000, 0xFFFFD}, + {0x100000, 0x10FFFD} +}; + +/* Unassigned characters. */ +static const struct widechar_range widechar_unassigned_table[] = { + {0x00378, 0x00379}, + {0x00380, 0x00383}, + {0x0038B, 0x0038B}, + {0x0038D, 0x0038D}, + {0x003A2, 0x003A2}, + {0x00530, 0x00530}, + {0x00557, 0x00558}, + {0x0058B, 0x0058C}, + {0x00590, 0x00590}, + {0x005C8, 0x005CF}, + {0x005EB, 0x005EE}, + {0x005F5, 0x005FF}, + {0x0070E, 0x0070E}, + {0x0074B, 0x0074C}, + {0x007B2, 0x007BF}, + {0x007FB, 0x007FC}, + {0x0082E, 0x0082F}, + {0x0083F, 0x0083F}, + {0x0085C, 0x0085D}, + {0x0085F, 0x0085F}, + {0x0086B, 0x0086F}, + {0x0088F, 0x0088F}, + {0x00892, 0x00896}, + {0x00984, 0x00984}, + {0x0098D, 0x0098E}, + {0x00991, 0x00992}, + {0x009A9, 0x009A9}, + {0x009B1, 0x009B1}, + {0x009B3, 0x009B5}, + {0x009BA, 0x009BB}, + {0x009C5, 0x009C6}, + {0x009C9, 0x009CA}, + {0x009CF, 0x009D6}, + {0x009D8, 0x009DB}, + {0x009DE, 0x009DE}, + {0x009E4, 0x009E5}, + {0x009FF, 0x00A00}, + {0x00A04, 0x00A04}, + {0x00A0B, 0x00A0E}, + {0x00A11, 0x00A12}, + {0x00A29, 0x00A29}, + {0x00A31, 0x00A31}, + {0x00A34, 0x00A34}, + {0x00A37, 0x00A37}, + {0x00A3A, 0x00A3B}, + {0x00A3D, 0x00A3D}, + {0x00A43, 0x00A46}, + {0x00A49, 0x00A4A}, + {0x00A4E, 0x00A50}, + {0x00A52, 0x00A58}, + {0x00A5D, 0x00A5D}, + {0x00A5F, 0x00A65}, + {0x00A77, 0x00A80}, + {0x00A84, 0x00A84}, + {0x00A8E, 0x00A8E}, + {0x00A92, 0x00A92}, + {0x00AA9, 0x00AA9}, + {0x00AB1, 0x00AB1}, + {0x00AB4, 0x00AB4}, + {0x00ABA, 0x00ABB}, + {0x00AC6, 0x00AC6}, + {0x00ACA, 0x00ACA}, + {0x00ACE, 0x00ACF}, + {0x00AD1, 0x00ADF}, + {0x00AE4, 0x00AE5}, + {0x00AF2, 0x00AF8}, + {0x00B00, 0x00B00}, + {0x00B04, 0x00B04}, + {0x00B0D, 0x00B0E}, + {0x00B11, 0x00B12}, + {0x00B29, 0x00B29}, + {0x00B31, 0x00B31}, + {0x00B34, 0x00B34}, + {0x00B3A, 0x00B3B}, + {0x00B45, 0x00B46}, + {0x00B49, 0x00B4A}, + {0x00B4E, 0x00B54}, + {0x00B58, 0x00B5B}, + {0x00B5E, 0x00B5E}, + {0x00B64, 0x00B65}, + {0x00B78, 0x00B81}, + {0x00B84, 0x00B84}, + {0x00B8B, 0x00B8D}, + {0x00B91, 0x00B91}, + {0x00B96, 0x00B98}, + {0x00B9B, 0x00B9B}, + {0x00B9D, 0x00B9D}, + {0x00BA0, 0x00BA2}, + {0x00BA5, 0x00BA7}, + {0x00BAB, 0x00BAD}, + {0x00BBA, 0x00BBD}, + {0x00BC3, 0x00BC5}, + {0x00BC9, 0x00BC9}, + {0x00BCE, 0x00BCF}, + {0x00BD1, 0x00BD6}, + {0x00BD8, 0x00BE5}, + {0x00BFB, 0x00BFF}, + {0x00C0D, 0x00C0D}, + {0x00C11, 0x00C11}, + {0x00C29, 0x00C29}, + {0x00C3A, 0x00C3B}, + {0x00C45, 0x00C45}, + {0x00C49, 0x00C49}, + {0x00C4E, 0x00C54}, + {0x00C57, 0x00C57}, + {0x00C5B, 0x00C5C}, + {0x00C5E, 0x00C5F}, + {0x00C64, 0x00C65}, + {0x00C70, 0x00C76}, + {0x00C8D, 0x00C8D}, + {0x00C91, 0x00C91}, + {0x00CA9, 0x00CA9}, + {0x00CB4, 0x00CB4}, + {0x00CBA, 0x00CBB}, + {0x00CC5, 0x00CC5}, + {0x00CC9, 0x00CC9}, + {0x00CCE, 0x00CD4}, + {0x00CD7, 0x00CDC}, + {0x00CDF, 0x00CDF}, + {0x00CE4, 0x00CE5}, + {0x00CF0, 0x00CF0}, + {0x00CF4, 0x00CFF}, + {0x00D0D, 0x00D0D}, + {0x00D11, 0x00D11}, + {0x00D45, 0x00D45}, + {0x00D49, 0x00D49}, + {0x00D50, 0x00D53}, + {0x00D64, 0x00D65}, + {0x00D80, 0x00D80}, + {0x00D84, 0x00D84}, + {0x00D97, 0x00D99}, + {0x00DB2, 0x00DB2}, + {0x00DBC, 0x00DBC}, + {0x00DBE, 0x00DBF}, + {0x00DC7, 0x00DC9}, + {0x00DCB, 0x00DCE}, + {0x00DD5, 0x00DD5}, + {0x00DD7, 0x00DD7}, + {0x00DE0, 0x00DE5}, + {0x00DF0, 0x00DF1}, + {0x00DF5, 0x00E00}, + {0x00E3B, 0x00E3E}, + {0x00E5C, 0x00E80}, + {0x00E83, 0x00E83}, + {0x00E85, 0x00E85}, + {0x00E8B, 0x00E8B}, + {0x00EA4, 0x00EA4}, + {0x00EA6, 0x00EA6}, + {0x00EBE, 0x00EBF}, + {0x00EC5, 0x00EC5}, + {0x00EC7, 0x00EC7}, + {0x00ECF, 0x00ECF}, + {0x00EDA, 0x00EDB}, + {0x00EE0, 0x00EFF}, + {0x00F48, 0x00F48}, + {0x00F6D, 0x00F70}, + {0x00F98, 0x00F98}, + {0x00FBD, 0x00FBD}, + {0x00FCD, 0x00FCD}, + {0x00FDB, 0x00FFF}, + {0x010C6, 0x010C6}, + {0x010C8, 0x010CC}, + {0x010CE, 0x010CF}, + {0x01249, 0x01249}, + {0x0124E, 0x0124F}, + {0x01257, 0x01257}, + {0x01259, 0x01259}, + {0x0125E, 0x0125F}, + {0x01289, 0x01289}, + {0x0128E, 0x0128F}, + {0x012B1, 0x012B1}, + {0x012B6, 0x012B7}, + {0x012BF, 0x012BF}, + {0x012C1, 0x012C1}, + {0x012C6, 0x012C7}, + {0x012D7, 0x012D7}, + {0x01311, 0x01311}, + {0x01316, 0x01317}, + {0x0135B, 0x0135C}, + {0x0137D, 0x0137F}, + {0x0139A, 0x0139F}, + {0x013F6, 0x013F7}, + {0x013FE, 0x013FF}, + {0x0169D, 0x0169F}, + {0x016F9, 0x016FF}, + {0x01716, 0x0171E}, + {0x01737, 0x0173F}, + {0x01754, 0x0175F}, + {0x0176D, 0x0176D}, + {0x01771, 0x01771}, + {0x01774, 0x0177F}, + {0x017DE, 0x017DF}, + {0x017EA, 0x017EF}, + {0x017FA, 0x017FF}, + {0x0181A, 0x0181F}, + {0x01879, 0x0187F}, + {0x018AB, 0x018AF}, + {0x018F6, 0x018FF}, + {0x0191F, 0x0191F}, + {0x0192C, 0x0192F}, + {0x0193C, 0x0193F}, + {0x01941, 0x01943}, + {0x0196E, 0x0196F}, + {0x01975, 0x0197F}, + {0x019AC, 0x019AF}, + {0x019CA, 0x019CF}, + {0x019DB, 0x019DD}, + {0x01A1C, 0x01A1D}, + {0x01A5F, 0x01A5F}, + {0x01A7D, 0x01A7E}, + {0x01A8A, 0x01A8F}, + {0x01A9A, 0x01A9F}, + {0x01AAE, 0x01AAF}, + {0x01ACF, 0x01AFF}, + {0x01B4D, 0x01B4D}, + {0x01BF4, 0x01BFB}, + {0x01C38, 0x01C3A}, + {0x01C4A, 0x01C4C}, + {0x01C8B, 0x01C8F}, + {0x01CBB, 0x01CBC}, + {0x01CC8, 0x01CCF}, + {0x01CFB, 0x01CFF}, + {0x01F16, 0x01F17}, + {0x01F1E, 0x01F1F}, + {0x01F46, 0x01F47}, + {0x01F4E, 0x01F4F}, + {0x01F58, 0x01F58}, + {0x01F5A, 0x01F5A}, + {0x01F5C, 0x01F5C}, + {0x01F5E, 0x01F5E}, + {0x01F7E, 0x01F7F}, + {0x01FB5, 0x01FB5}, + {0x01FC5, 0x01FC5}, + {0x01FD4, 0x01FD5}, + {0x01FDC, 0x01FDC}, + {0x01FF0, 0x01FF1}, + {0x01FF5, 0x01FF5}, + {0x01FFF, 0x01FFF}, + {0x02065, 0x02065}, + {0x02072, 0x02073}, + {0x0208F, 0x0208F}, + {0x0209D, 0x0209F}, + {0x020C1, 0x020CF}, + {0x020F1, 0x020FF}, + {0x0218C, 0x0218F}, + {0x0242A, 0x0243F}, + {0x0244B, 0x0245F}, + {0x02B74, 0x02B75}, + {0x02B96, 0x02B96}, + {0x02CF4, 0x02CF8}, + {0x02D26, 0x02D26}, + {0x02D28, 0x02D2C}, + {0x02D2E, 0x02D2F}, + {0x02D68, 0x02D6E}, + {0x02D71, 0x02D7E}, + {0x02D97, 0x02D9F}, + {0x02DA7, 0x02DA7}, + {0x02DAF, 0x02DAF}, + {0x02DB7, 0x02DB7}, + {0x02DBF, 0x02DBF}, + {0x02DC7, 0x02DC7}, + {0x02DCF, 0x02DCF}, + {0x02DD7, 0x02DD7}, + {0x02DDF, 0x02DDF}, + {0x02E5E, 0x02E7F}, + {0x02E9A, 0x02E9A}, + {0x02EF4, 0x02EFF}, + {0x02FD6, 0x02FEF}, + {0x03040, 0x03040}, + {0x03097, 0x03098}, + {0x03100, 0x03104}, + {0x03130, 0x03130}, + {0x0318F, 0x0318F}, + {0x031E6, 0x031EE}, + {0x0321F, 0x0321F}, + {0x03401, 0x04DBE}, + {0x04E01, 0x09FFE}, + {0x0A48D, 0x0A48F}, + {0x0A4C7, 0x0A4CF}, + {0x0A62C, 0x0A63F}, + {0x0A6F8, 0x0A6FF}, + {0x0A7CE, 0x0A7CF}, + {0x0A7D2, 0x0A7D2}, + {0x0A7D4, 0x0A7D4}, + {0x0A7DD, 0x0A7F1}, + {0x0A82D, 0x0A82F}, + {0x0A83A, 0x0A83F}, + {0x0A878, 0x0A87F}, + {0x0A8C6, 0x0A8CD}, + {0x0A8DA, 0x0A8DF}, + {0x0A954, 0x0A95E}, + {0x0A97D, 0x0A97F}, + {0x0A9CE, 0x0A9CE}, + {0x0A9DA, 0x0A9DD}, + {0x0A9FF, 0x0A9FF}, + {0x0AA37, 0x0AA3F}, + {0x0AA4E, 0x0AA4F}, + {0x0AA5A, 0x0AA5B}, + {0x0AAC3, 0x0AADA}, + {0x0AAF7, 0x0AB00}, + {0x0AB07, 0x0AB08}, + {0x0AB0F, 0x0AB10}, + {0x0AB17, 0x0AB1F}, + {0x0AB27, 0x0AB27}, + {0x0AB2F, 0x0AB2F}, + {0x0AB6C, 0x0AB6F}, + {0x0ABEE, 0x0ABEF}, + {0x0ABFA, 0x0ABFF}, + {0x0AC01, 0x0D7A2}, + {0x0D7A4, 0x0D7AF}, + {0x0D7C7, 0x0D7CA}, + {0x0D7FC, 0x0D7FF}, + {0x0FA6E, 0x0FA6F}, + {0x0FADA, 0x0FAFF}, + {0x0FB07, 0x0FB12}, + {0x0FB18, 0x0FB1C}, + {0x0FB37, 0x0FB37}, + {0x0FB3D, 0x0FB3D}, + {0x0FB3F, 0x0FB3F}, + {0x0FB42, 0x0FB42}, + {0x0FB45, 0x0FB45}, + {0x0FBC3, 0x0FBD2}, + {0x0FD90, 0x0FD91}, + {0x0FDC8, 0x0FDCE}, + {0x0FE1A, 0x0FE1F}, + {0x0FE53, 0x0FE53}, + {0x0FE67, 0x0FE67}, + {0x0FE6C, 0x0FE6F}, + {0x0FE75, 0x0FE75}, + {0x0FEFD, 0x0FEFE}, + {0x0FF00, 0x0FF00}, + {0x0FFBF, 0x0FFC1}, + {0x0FFC8, 0x0FFC9}, + {0x0FFD0, 0x0FFD1}, + {0x0FFD8, 0x0FFD9}, + {0x0FFDD, 0x0FFDF}, + {0x0FFE7, 0x0FFE7}, + {0x0FFEF, 0x0FFF8}, + {0x1000C, 0x1000C}, + {0x10027, 0x10027}, + {0x1003B, 0x1003B}, + {0x1003E, 0x1003E}, + {0x1004E, 0x1004F}, + {0x1005E, 0x1007F}, + {0x100FB, 0x100FF}, + {0x10103, 0x10106}, + {0x10134, 0x10136}, + {0x1018F, 0x1018F}, + {0x1019D, 0x1019F}, + {0x101A1, 0x101CF}, + {0x101FE, 0x1027F}, + {0x1029D, 0x1029F}, + {0x102D1, 0x102DF}, + {0x102FC, 0x102FF}, + {0x10324, 0x1032C}, + {0x1034B, 0x1034F}, + {0x1037B, 0x1037F}, + {0x1039E, 0x1039E}, + {0x103C4, 0x103C7}, + {0x103D6, 0x103FF}, + {0x1049E, 0x1049F}, + {0x104AA, 0x104AF}, + {0x104D4, 0x104D7}, + {0x104FC, 0x104FF}, + {0x10528, 0x1052F}, + {0x10564, 0x1056E}, + {0x1057B, 0x1057B}, + {0x1058B, 0x1058B}, + {0x10593, 0x10593}, + {0x10596, 0x10596}, + {0x105A2, 0x105A2}, + {0x105B2, 0x105B2}, + {0x105BA, 0x105BA}, + {0x105BD, 0x105BF}, + {0x105F4, 0x105FF}, + {0x10737, 0x1073F}, + {0x10756, 0x1075F}, + {0x10768, 0x1077F}, + {0x10786, 0x10786}, + {0x107B1, 0x107B1}, + {0x107BB, 0x107FF}, + {0x10806, 0x10807}, + {0x10809, 0x10809}, + {0x10836, 0x10836}, + {0x10839, 0x1083B}, + {0x1083D, 0x1083E}, + {0x10856, 0x10856}, + {0x1089F, 0x108A6}, + {0x108B0, 0x108DF}, + {0x108F3, 0x108F3}, + {0x108F6, 0x108FA}, + {0x1091C, 0x1091E}, + {0x1093A, 0x1093E}, + {0x10940, 0x1097F}, + {0x109B8, 0x109BB}, + {0x109D0, 0x109D1}, + {0x10A04, 0x10A04}, + {0x10A07, 0x10A0B}, + {0x10A14, 0x10A14}, + {0x10A18, 0x10A18}, + {0x10A36, 0x10A37}, + {0x10A3B, 0x10A3E}, + {0x10A49, 0x10A4F}, + {0x10A59, 0x10A5F}, + {0x10AA0, 0x10ABF}, + {0x10AE7, 0x10AEA}, + {0x10AF7, 0x10AFF}, + {0x10B36, 0x10B38}, + {0x10B56, 0x10B57}, + {0x10B73, 0x10B77}, + {0x10B92, 0x10B98}, + {0x10B9D, 0x10BA8}, + {0x10BB0, 0x10BFF}, + {0x10C49, 0x10C7F}, + {0x10CB3, 0x10CBF}, + {0x10CF3, 0x10CF9}, + {0x10D28, 0x10D2F}, + {0x10D3A, 0x10D3F}, + {0x10D66, 0x10D68}, + {0x10D86, 0x10D8D}, + {0x10D90, 0x10E5F}, + {0x10E7F, 0x10E7F}, + {0x10EAA, 0x10EAA}, + {0x10EAE, 0x10EAF}, + {0x10EB2, 0x10EC1}, + {0x10EC5, 0x10EFB}, + {0x10F28, 0x10F2F}, + {0x10F5A, 0x10F6F}, + {0x10F8A, 0x10FAF}, + {0x10FCC, 0x10FDF}, + {0x10FF7, 0x10FFF}, + {0x1104E, 0x11051}, + {0x11076, 0x1107E}, + {0x110C3, 0x110CC}, + {0x110CE, 0x110CF}, + {0x110E9, 0x110EF}, + {0x110FA, 0x110FF}, + {0x11135, 0x11135}, + {0x11148, 0x1114F}, + {0x11177, 0x1117F}, + {0x111E0, 0x111E0}, + {0x111F5, 0x111FF}, + {0x11212, 0x11212}, + {0x11242, 0x1127F}, + {0x11287, 0x11287}, + {0x11289, 0x11289}, + {0x1128E, 0x1128E}, + {0x1129E, 0x1129E}, + {0x112AA, 0x112AF}, + {0x112EB, 0x112EF}, + {0x112FA, 0x112FF}, + {0x11304, 0x11304}, + {0x1130D, 0x1130E}, + {0x11311, 0x11312}, + {0x11329, 0x11329}, + {0x11331, 0x11331}, + {0x11334, 0x11334}, + {0x1133A, 0x1133A}, + {0x11345, 0x11346}, + {0x11349, 0x1134A}, + {0x1134E, 0x1134F}, + {0x11351, 0x11356}, + {0x11358, 0x1135C}, + {0x11364, 0x11365}, + {0x1136D, 0x1136F}, + {0x11375, 0x1137F}, + {0x1138A, 0x1138A}, + {0x1138C, 0x1138D}, + {0x1138F, 0x1138F}, + {0x113B6, 0x113B6}, + {0x113C1, 0x113C1}, + {0x113C3, 0x113C4}, + {0x113C6, 0x113C6}, + {0x113CB, 0x113CB}, + {0x113D6, 0x113D6}, + {0x113D9, 0x113E0}, + {0x113E3, 0x113FF}, + {0x1145C, 0x1145C}, + {0x11462, 0x1147F}, + {0x114C8, 0x114CF}, + {0x114DA, 0x1157F}, + {0x115B6, 0x115B7}, + {0x115DE, 0x115FF}, + {0x11645, 0x1164F}, + {0x1165A, 0x1165F}, + {0x1166D, 0x1167F}, + {0x116BA, 0x116BF}, + {0x116CA, 0x116CF}, + {0x116E4, 0x116FF}, + {0x1171B, 0x1171C}, + {0x1172C, 0x1172F}, + {0x11747, 0x117FF}, + {0x1183C, 0x1189F}, + {0x118F3, 0x118FE}, + {0x11907, 0x11908}, + {0x1190A, 0x1190B}, + {0x11914, 0x11914}, + {0x11917, 0x11917}, + {0x11936, 0x11936}, + {0x11939, 0x1193A}, + {0x11947, 0x1194F}, + {0x1195A, 0x1199F}, + {0x119A8, 0x119A9}, + {0x119D8, 0x119D9}, + {0x119E5, 0x119FF}, + {0x11A48, 0x11A4F}, + {0x11AA3, 0x11AAF}, + {0x11AF9, 0x11AFF}, + {0x11B0A, 0x11BBF}, + {0x11BE2, 0x11BEF}, + {0x11BFA, 0x11BFF}, + {0x11C09, 0x11C09}, + {0x11C37, 0x11C37}, + {0x11C46, 0x11C4F}, + {0x11C6D, 0x11C6F}, + {0x11C90, 0x11C91}, + {0x11CA8, 0x11CA8}, + {0x11CB7, 0x11CFF}, + {0x11D07, 0x11D07}, + {0x11D0A, 0x11D0A}, + {0x11D37, 0x11D39}, + {0x11D3B, 0x11D3B}, + {0x11D3E, 0x11D3E}, + {0x11D48, 0x11D4F}, + {0x11D5A, 0x11D5F}, + {0x11D66, 0x11D66}, + {0x11D69, 0x11D69}, + {0x11D8F, 0x11D8F}, + {0x11D92, 0x11D92}, + {0x11D99, 0x11D9F}, + {0x11DAA, 0x11EDF}, + {0x11EF9, 0x11EFF}, + {0x11F11, 0x11F11}, + {0x11F3B, 0x11F3D}, + {0x11F5B, 0x11FAF}, + {0x11FB1, 0x11FBF}, + {0x11FF2, 0x11FFE}, + {0x1239A, 0x123FF}, + {0x1246F, 0x1246F}, + {0x12475, 0x1247F}, + {0x12544, 0x12F8F}, + {0x12FF3, 0x12FFF}, + {0x13456, 0x1345F}, + {0x143FB, 0x143FF}, + {0x14647, 0x160FF}, + {0x1613A, 0x167FF}, + {0x16A39, 0x16A3F}, + {0x16A5F, 0x16A5F}, + {0x16A6A, 0x16A6D}, + {0x16ABF, 0x16ABF}, + {0x16ACA, 0x16ACF}, + {0x16AEE, 0x16AEF}, + {0x16AF6, 0x16AFF}, + {0x16B46, 0x16B4F}, + {0x16B5A, 0x16B5A}, + {0x16B62, 0x16B62}, + {0x16B78, 0x16B7C}, + {0x16B90, 0x16D3F}, + {0x16D7A, 0x16E3F}, + {0x16E9B, 0x16EFF}, + {0x16F4B, 0x16F4E}, + {0x16F88, 0x16F8E}, + {0x16FA0, 0x16FDF}, + {0x16FE5, 0x16FEF}, + {0x16FF2, 0x16FFF}, + {0x17001, 0x187F6}, + {0x187F8, 0x187FF}, + {0x18CD6, 0x18CFE}, + {0x18D01, 0x18D07}, + {0x18D09, 0x1AFEF}, + {0x1AFF4, 0x1AFF4}, + {0x1AFFC, 0x1AFFC}, + {0x1AFFF, 0x1AFFF}, + {0x1B123, 0x1B131}, + {0x1B133, 0x1B14F}, + {0x1B153, 0x1B154}, + {0x1B156, 0x1B163}, + {0x1B168, 0x1B16F}, + {0x1B2FC, 0x1BBFF}, + {0x1BC6B, 0x1BC6F}, + {0x1BC7D, 0x1BC7F}, + {0x1BC89, 0x1BC8F}, + {0x1BC9A, 0x1BC9B}, + {0x1BCA4, 0x1CBFF}, + {0x1CCFA, 0x1CCFF}, + {0x1CEB4, 0x1CEFF}, + {0x1CF2E, 0x1CF2F}, + {0x1CF47, 0x1CF4F}, + {0x1CFC4, 0x1CFFF}, + {0x1D0F6, 0x1D0FF}, + {0x1D127, 0x1D128}, + {0x1D1EB, 0x1D1FF}, + {0x1D246, 0x1D2BF}, + {0x1D2D4, 0x1D2DF}, + {0x1D2F4, 0x1D2FF}, + {0x1D357, 0x1D35F}, + {0x1D379, 0x1D3FF}, + {0x1D455, 0x1D455}, + {0x1D49D, 0x1D49D}, + {0x1D4A0, 0x1D4A1}, + {0x1D4A3, 0x1D4A4}, + {0x1D4A7, 0x1D4A8}, + {0x1D4AD, 0x1D4AD}, + {0x1D4BA, 0x1D4BA}, + {0x1D4BC, 0x1D4BC}, + {0x1D4C4, 0x1D4C4}, + {0x1D506, 0x1D506}, + {0x1D50B, 0x1D50C}, + {0x1D515, 0x1D515}, + {0x1D51D, 0x1D51D}, + {0x1D53A, 0x1D53A}, + {0x1D53F, 0x1D53F}, + {0x1D545, 0x1D545}, + {0x1D547, 0x1D549}, + {0x1D551, 0x1D551}, + {0x1D6A6, 0x1D6A7}, + {0x1D7CC, 0x1D7CD}, + {0x1DA8C, 0x1DA9A}, + {0x1DAA0, 0x1DAA0}, + {0x1DAB0, 0x1DEFF}, + {0x1DF1F, 0x1DF24}, + {0x1DF2B, 0x1DFFF}, + {0x1E007, 0x1E007}, + {0x1E019, 0x1E01A}, + {0x1E022, 0x1E022}, + {0x1E025, 0x1E025}, + {0x1E02B, 0x1E02F}, + {0x1E06E, 0x1E08E}, + {0x1E090, 0x1E0FF}, + {0x1E12D, 0x1E12F}, + {0x1E13E, 0x1E13F}, + {0x1E14A, 0x1E14D}, + {0x1E150, 0x1E28F}, + {0x1E2AF, 0x1E2BF}, + {0x1E2FA, 0x1E2FE}, + {0x1E300, 0x1E4CF}, + {0x1E4FA, 0x1E5CF}, + {0x1E5FB, 0x1E5FE}, + {0x1E600, 0x1E7DF}, + {0x1E7E7, 0x1E7E7}, + {0x1E7EC, 0x1E7EC}, + {0x1E7EF, 0x1E7EF}, + {0x1E7FF, 0x1E7FF}, + {0x1E8C5, 0x1E8C6}, + {0x1E8D7, 0x1E8FF}, + {0x1E94C, 0x1E94F}, + {0x1E95A, 0x1E95D}, + {0x1E960, 0x1EC70}, + {0x1ECB5, 0x1ED00}, + {0x1ED3E, 0x1EDFF}, + {0x1EE04, 0x1EE04}, + {0x1EE20, 0x1EE20}, + {0x1EE23, 0x1EE23}, + {0x1EE25, 0x1EE26}, + {0x1EE28, 0x1EE28}, + {0x1EE33, 0x1EE33}, + {0x1EE38, 0x1EE38}, + {0x1EE3A, 0x1EE3A}, + {0x1EE3C, 0x1EE41}, + {0x1EE43, 0x1EE46}, + {0x1EE48, 0x1EE48}, + {0x1EE4A, 0x1EE4A}, + {0x1EE4C, 0x1EE4C}, + {0x1EE50, 0x1EE50}, + {0x1EE53, 0x1EE53}, + {0x1EE55, 0x1EE56}, + {0x1EE58, 0x1EE58}, + {0x1EE5A, 0x1EE5A}, + {0x1EE5C, 0x1EE5C}, + {0x1EE5E, 0x1EE5E}, + {0x1EE60, 0x1EE60}, + {0x1EE63, 0x1EE63}, + {0x1EE65, 0x1EE66}, + {0x1EE6B, 0x1EE6B}, + {0x1EE73, 0x1EE73}, + {0x1EE78, 0x1EE78}, + {0x1EE7D, 0x1EE7D}, + {0x1EE7F, 0x1EE7F}, + {0x1EE8A, 0x1EE8A}, + {0x1EE9C, 0x1EEA0}, + {0x1EEA4, 0x1EEA4}, + {0x1EEAA, 0x1EEAA}, + {0x1EEBC, 0x1EEEF}, + {0x1EEF2, 0x1EFFF}, + {0x1F02C, 0x1F02F}, + {0x1F094, 0x1F09F}, + {0x1F0AF, 0x1F0B0}, + {0x1F0C0, 0x1F0C0}, + {0x1F0D0, 0x1F0D0}, + {0x1F0F6, 0x1F0FF}, + {0x1F1AE, 0x1F1E5}, + {0x1F203, 0x1F20F}, + {0x1F23C, 0x1F23F}, + {0x1F249, 0x1F24F}, + {0x1F252, 0x1F25F}, + {0x1F266, 0x1F2FF}, + {0x1F6D8, 0x1F6DB}, + {0x1F6ED, 0x1F6EF}, + {0x1F6FD, 0x1F6FF}, + {0x1F777, 0x1F77A}, + {0x1F7DA, 0x1F7DF}, + {0x1F7EC, 0x1F7EF}, + {0x1F7F1, 0x1F7FF}, + {0x1F80C, 0x1F80F}, + {0x1F848, 0x1F84F}, + {0x1F85A, 0x1F85F}, + {0x1F888, 0x1F88F}, + {0x1F8AE, 0x1F8AF}, + {0x1F8BC, 0x1F8BF}, + {0x1F8C2, 0x1F8FF}, + {0x1FA54, 0x1FA5F}, + {0x1FA6E, 0x1FA6F}, + {0x1FA7D, 0x1FA7F}, + {0x1FA8A, 0x1FA8E}, + {0x1FAC7, 0x1FACD}, + {0x1FADD, 0x1FADE}, + {0x1FAEA, 0x1FAEF}, + {0x1FAF9, 0x1FAFF}, + {0x1FB93, 0x1FB93}, + {0x1FBFA, 0x1FFFD}, + {0x20001, 0x2A6DE}, + {0x2A6E0, 0x2A6FF}, + {0x2A701, 0x2B738}, + {0x2B73A, 0x2B73F}, + {0x2B741, 0x2B81C}, + {0x2B81E, 0x2B81F}, + {0x2B821, 0x2CEA0}, + {0x2CEA2, 0x2CEAF}, + {0x2CEB1, 0x2EBDF}, + {0x2EBE1, 0x2EBEF}, + {0x2EBF1, 0x2EE5C}, + {0x2EE5E, 0x2F7FF}, + {0x2FA1E, 0x2FFFD}, + {0x30001, 0x31349}, + {0x3134B, 0x3134F}, + {0x31351, 0x323AE}, + {0x323B0, 0x3FFFD}, + {0x40000, 0x4FFFD}, + {0x50000, 0x5FFFD}, + {0x60000, 0x6FFFD}, + {0x70000, 0x7FFFD}, + {0x80000, 0x8FFFD}, + {0x90000, 0x9FFFD}, + {0xA0000, 0xAFFFD}, + {0xB0000, 0xBFFFD}, + {0xC0000, 0xCFFFD}, + {0xD0000, 0xDFFFD}, + {0xE0000, 0xE0000}, + {0xE0002, 0xE001F}, + {0xE0080, 0xE00FF}, + {0xE01F0, 0xEFFFD} +}; + +/* Non-characters. */ +static const struct widechar_range widechar_nonchar_table[] = { + {0x0FDD0, 0x0FDEF}, + {0x0FFFE, 0x0FFFF}, + {0x1FFFE, 0x1FFFF}, + {0x2FFFE, 0x2FFFF}, + {0x3FFFE, 0x3FFFF}, + {0x4FFFE, 0x4FFFF}, + {0x5FFFE, 0x5FFFF}, + {0x6FFFE, 0x6FFFF}, + {0x7FFFE, 0x7FFFF}, + {0x8FFFE, 0x8FFFF}, + {0x9FFFE, 0x9FFFF}, + {0xAFFFE, 0xAFFFF}, + {0xBFFFE, 0xBFFFF}, + {0xCFFFE, 0xCFFFF}, + {0xDFFFE, 0xDFFFF}, + {0xEFFFE, 0xEFFFF}, + {0xFFFFE, 0xFFFFF}, + {0x10FFFE, 0x10FFFF} +}; + +/* Characters that were widened from width 1 to 2 in Unicode 9. */ +static const struct widechar_range widechar_widened_table[] = { + {0x0231A, 0x0231B}, + {0x023E9, 0x023EC}, + {0x023F0, 0x023F0}, + {0x023F3, 0x023F3}, + {0x025FD, 0x025FE}, + {0x02614, 0x02615}, + {0x02648, 0x02653}, + {0x0267F, 0x0267F}, + {0x02693, 0x02693}, + {0x026A1, 0x026A1}, + {0x026AA, 0x026AB}, + {0x026BD, 0x026BE}, + {0x026C4, 0x026C5}, + {0x026CE, 0x026CE}, + {0x026D4, 0x026D4}, + {0x026EA, 0x026EA}, + {0x026F2, 0x026F3}, + {0x026F5, 0x026F5}, + {0x026FA, 0x026FA}, + {0x026FD, 0x026FD}, + {0x02705, 0x02705}, + {0x0270A, 0x0270B}, + {0x02728, 0x02728}, + {0x0274C, 0x0274C}, + {0x0274E, 0x0274E}, + {0x02753, 0x02755}, + {0x02757, 0x02757}, + {0x02795, 0x02797}, + {0x027B0, 0x027B0}, + {0x027BF, 0x027BF}, + {0x02B1B, 0x02B1C}, + {0x02B50, 0x02B50}, + {0x02B55, 0x02B55}, + {0x1F004, 0x1F004}, + {0x1F0CF, 0x1F0CF}, + {0x1F18E, 0x1F18E}, + {0x1F191, 0x1F19A}, + {0x1F201, 0x1F201}, + {0x1F21A, 0x1F21A}, + {0x1F22F, 0x1F22F}, + {0x1F232, 0x1F236}, + {0x1F238, 0x1F23A}, + {0x1F250, 0x1F251}, + {0x1F300, 0x1F320}, + {0x1F32D, 0x1F335}, + {0x1F337, 0x1F37C}, + {0x1F37E, 0x1F393}, + {0x1F3A0, 0x1F3CA}, + {0x1F3CF, 0x1F3D3}, + {0x1F3E0, 0x1F3F0}, + {0x1F3F4, 0x1F3F4}, + {0x1F3F8, 0x1F43E}, + {0x1F440, 0x1F440}, + {0x1F442, 0x1F4FC}, + {0x1F4FF, 0x1F53D}, + {0x1F54B, 0x1F54E}, + {0x1F550, 0x1F567}, + {0x1F595, 0x1F596}, + {0x1F5FB, 0x1F64F}, + {0x1F680, 0x1F6C5}, + {0x1F6CC, 0x1F6CC}, + {0x1F6D0, 0x1F6D0}, + {0x1F6EB, 0x1F6EC}, + {0x1F910, 0x1F918}, + {0x1F980, 0x1F984}, + {0x1F9C0, 0x1F9C0} +}; + +template +bool widechar_in_table(const Collection &arr, uint32_t c) { + auto where = std::lower_bound(std::begin(arr), std::end(arr), c, + [](widechar_range p, uint32_t c) { return p.hi < c; }); + return where != std::end(arr) && where->lo <= c; +} + +/* Return the width of character c, or a special negative value. */ +int widechar_wcwidth(uint32_t c) { + if (widechar_in_table(widechar_ascii_table, c)) + return 1; + if (widechar_in_table(widechar_private_table, c)) + return widechar_private_use; + if (widechar_in_table(widechar_nonprint_table, c)) + return widechar_nonprint; + if (widechar_in_table(widechar_nonchar_table, c)) + return widechar_non_character; + if (widechar_in_table(widechar_combining_table, c)) + return widechar_combining; + if (widechar_in_table(widechar_combiningletters_table, c)) + return widechar_combining; + if (widechar_in_table(widechar_doublewide_table, c)) + return 2; + if (widechar_in_table(widechar_ambiguous_table, c)) + return widechar_ambiguous; + if (widechar_in_table(widechar_unassigned_table, c)) + return widechar_unassigned; + if (widechar_in_table(widechar_widened_table, c)) + return widechar_widened_in_9; + return 1; +} + +} // namespace +#endif // WIDECHAR_WIDTH_H diff --git a/src/libutil/windows/environment-variables.cc b/src/libutil/windows/environment-variables.cc index 5ce8a139537..d7cc7b488c7 100644 --- a/src/libutil/windows/environment-variables.cc +++ b/src/libutil/windows/environment-variables.cc @@ -1,6 +1,7 @@ -#include "environment-variables.hh" +#include "nix/util/environment-variables.hh" -#include "processenv.h" +#ifdef _WIN32 +# include "processenv.h" namespace nix { @@ -12,8 +13,10 @@ std::optional getEnvOs(const OsString & key) return std::nullopt; } - // Allocate a buffer to hold the environment variable value - std::wstring value{bufferSize, L'\0'}; + /* Allocate a buffer to hold the environment variable value. + WARNING: Do not even think about using uniform initialization here, + we DONT want to call the initializer list ctor accidentally. */ + std::wstring value(bufferSize, L'\0'); // Retrieve the environment variable value DWORD resultSize = GetEnvironmentVariableW(key.c_str(), &value[0], bufferSize); @@ -43,3 +46,4 @@ int setEnvOs(const OsString & name, const OsString & value) } } +#endif diff --git a/src/libutil/windows/file-descriptor.cc b/src/libutil/windows/file-descriptor.cc index 7b8a712e879..03d68232c37 100644 --- a/src/libutil/windows/file-descriptor.cc +++ b/src/libutil/windows/file-descriptor.cc @@ -1,10 +1,11 @@ -#include "file-system.hh" -#include "signals.hh" -#include "finally.hh" -#include "serialise.hh" -#include "windows-error.hh" -#include "file-path.hh" - +#include "nix/util/file-system.hh" +#include "nix/util/signals.hh" +#include "nix/util/finally.hh" +#include "nix/util/serialise.hh" +#include "nix/util/windows-error.hh" +#include "nix/util/file-path.hh" + +#ifdef _WIN32 #include #include #include @@ -46,7 +47,7 @@ void writeFull(HANDLE handle, std::string_view s, bool allowInterrupts) if (allowInterrupts) checkInterrupt(); DWORD res; #if _WIN32_WINNT >= 0x0600 - auto path = handleToPath(handle); // debug; do it before becuase handleToPath changes lasterror + auto path = handleToPath(handle); // debug; do it before because handleToPath changes lasterror if (!WriteFile(handle, s.data(), s.size(), &res, NULL)) { throw WinError("writing to file %1%:%2%", handle, path); } @@ -152,3 +153,4 @@ Path windows::handleToPath(HANDLE handle) { #endif } +#endif diff --git a/src/libutil/windows/file-path.cc b/src/libutil/windows/file-path.cc index 7405c426b62..03cc5afe5e4 100644 --- a/src/libutil/windows/file-path.cc +++ b/src/libutil/windows/file-path.cc @@ -3,9 +3,9 @@ #include #include -#include "file-path.hh" -#include "file-path-impl.hh" -#include "util.hh" +#include "nix/util/file-path.hh" +#include "nix/util/file-path-impl.hh" +#include "nix/util/util.hh" namespace nix { diff --git a/src/libutil/windows/file-system.cc b/src/libutil/windows/file-system.cc index b15355efe88..f31c913f141 100644 --- a/src/libutil/windows/file-system.cc +++ b/src/libutil/windows/file-system.cc @@ -1,7 +1,21 @@ -#include "file-system.hh" +#include "nix/util/file-system.hh" +#include "nix/util/logging.hh" +#ifdef _WIN32 namespace nix { +void setWriteTime( + const std::filesystem::path & path, time_t accessedTime, time_t modificationTime, std::optional optIsSymlink) +{ + // FIXME use `std::filesystem::last_write_time`. + // + // Would be nice to use std::filesystem unconditionally, but + // doesn't support access time just modification time. + // + // System clock vs File clock issues also make that annoying. + warn("Changing file times is not yet implemented on Windows, path is %s", path); +} + Descriptor openDirectory(const std::filesystem::path & path) { return CreateFileW( @@ -15,3 +29,4 @@ Descriptor openDirectory(const std::filesystem::path & path) } } +#endif diff --git a/src/libutil/windows/include/nix/util/meson.build b/src/libutil/windows/include/nix/util/meson.build new file mode 100644 index 00000000000..5d0ace9292a --- /dev/null +++ b/src/libutil/windows/include/nix/util/meson.build @@ -0,0 +1,10 @@ +# Public headers directory + +include_dirs += include_directories('../..') + +headers += files( + 'signals-impl.hh', + 'windows-async-pipe.hh', + 'windows-error.hh', + # hack for trailing newline +) diff --git a/src/libutil/windows/signals-impl.hh b/src/libutil/windows/include/nix/util/signals-impl.hh similarity index 78% rename from src/libutil/windows/signals-impl.hh rename to src/libutil/windows/include/nix/util/signals-impl.hh index 26d2600bf04..55606debde7 100644 --- a/src/libutil/windows/signals-impl.hh +++ b/src/libutil/windows/include/nix/util/signals-impl.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "types.hh" +#include "nix/util/types.hh" namespace nix { @@ -22,7 +22,13 @@ inline void setInterruptThrown() /* Do nothing for now */ } -void inline checkInterrupt() +static inline bool isInterrupted() +{ + /* Do nothing for now */ + return false; +} + +inline void checkInterrupt() { /* Do nothing for now */ } diff --git a/src/libutil/windows/windows-async-pipe.hh b/src/libutil/windows/include/nix/util/windows-async-pipe.hh similarity index 89% rename from src/libutil/windows/windows-async-pipe.hh rename to src/libutil/windows/include/nix/util/windows-async-pipe.hh index 8f554e403f3..5bb0c35185d 100644 --- a/src/libutil/windows/windows-async-pipe.hh +++ b/src/libutil/windows/include/nix/util/windows-async-pipe.hh @@ -1,7 +1,8 @@ #pragma once ///@file -#include "file-descriptor.hh" +#include "nix/util/file-descriptor.hh" +#ifdef _WIN32 namespace nix::windows { @@ -25,3 +26,4 @@ public: }; } +#endif diff --git a/src/libutil/windows/windows-error.hh b/src/libutil/windows/include/nix/util/windows-error.hh similarity index 95% rename from src/libutil/windows/windows-error.hh rename to src/libutil/windows/include/nix/util/windows-error.hh index 624b4c4cb0b..abf979c6b71 100644 --- a/src/libutil/windows/windows-error.hh +++ b/src/libutil/windows/include/nix/util/windows-error.hh @@ -1,9 +1,10 @@ #pragma once ///@file +#ifdef _WIN32 #include -#include "error.hh" +#include "nix/util/error.hh" namespace nix::windows { @@ -49,3 +50,4 @@ private: }; } +#endif diff --git a/src/libutil/windows/meson.build b/src/libutil/windows/meson.build index 1c645fe0573..0c1cec49cac 100644 --- a/src/libutil/windows/meson.build +++ b/src/libutil/windows/meson.build @@ -11,10 +11,4 @@ sources += files( 'windows-error.cc', ) -include_dirs += include_directories('.') - -headers += files( - 'signals-impl.hh', - 'windows-async-pipe.hh', - 'windows-error.hh', -) +subdir('include/nix/util') diff --git a/src/libutil/windows/muxable-pipe.cc b/src/libutil/windows/muxable-pipe.cc index 91a321f7cfa..82ef4066556 100644 --- a/src/libutil/windows/muxable-pipe.cc +++ b/src/libutil/windows/muxable-pipe.cc @@ -1,9 +1,10 @@ -#include -#include "windows-error.hh" +#ifdef _WIN32 +# include +# include "nix/util/windows-error.hh" -#include "logging.hh" -#include "util.hh" -#include "muxable-pipe.hh" +# include "nix/util/logging.hh" +# include "nix/util/util.hh" +# include "nix/util/muxable-pipe.hh" namespace nix { @@ -68,3 +69,4 @@ void MuxablePipePollState::iterate( } } +#endif diff --git a/src/libutil/windows/os-string.cc b/src/libutil/windows/os-string.cc index 7507f9030da..8c8a27a9f10 100644 --- a/src/libutil/windows/os-string.cc +++ b/src/libutil/windows/os-string.cc @@ -3,9 +3,11 @@ #include #include -#include "file-path.hh" -#include "file-path-impl.hh" -#include "util.hh" +#include "nix/util/file-path.hh" +#include "nix/util/file-path-impl.hh" +#include "nix/util/util.hh" + +#ifdef _WIN32 namespace nix { @@ -22,3 +24,5 @@ std::filesystem::path::string_type string_to_os_string(std::string_view s) } } + +#endif diff --git a/src/libutil/windows/processes.cc b/src/libutil/windows/processes.cc index 7f34c5632e8..099dff31b0b 100644 --- a/src/libutil/windows/processes.cc +++ b/src/libutil/windows/processes.cc @@ -1,16 +1,16 @@ -#include "current-process.hh" -#include "environment-variables.hh" -#include "error.hh" -#include "executable-path.hh" -#include "file-descriptor.hh" -#include "file-path.hh" -#include "signals.hh" -#include "processes.hh" -#include "finally.hh" -#include "serialise.hh" -#include "file-system.hh" -#include "util.hh" -#include "windows-error.hh" +#include "nix/util/current-process.hh" +#include "nix/util/environment-variables.hh" +#include "nix/util/error.hh" +#include "nix/util/executable-path.hh" +#include "nix/util/file-descriptor.hh" +#include "nix/util/file-path.hh" +#include "nix/util/signals.hh" +#include "nix/util/processes.hh" +#include "nix/util/finally.hh" +#include "nix/util/serialise.hh" +#include "nix/util/file-system.hh" +#include "nix/util/util.hh" +#include "nix/util/windows-error.hh" #include #include @@ -23,6 +23,8 @@ #include #include +#ifdef _WIN32 + #define WIN32_LEAN_AND_MEAN #include @@ -310,11 +312,7 @@ void runProgram2(const RunOptions & options) // TODO: Implement shebang / program interpreter lookup on Windows auto interpreter = getProgramInterpreter(realProgram); - std::optional>> resumeLoggerDefer; - if (options.isInteractive) { - logger->pause(); - resumeLoggerDefer.emplace([]() { logger->resume(); }); - } + auto suspension = logger->suspendIf(options.isInteractive); Pid pid = spawnProcess(interpreter.has_value() ? *interpreter : realProgram, options, out, in); @@ -386,3 +384,5 @@ int execvpe(const wchar_t * file0, const wchar_t * const argv[], const wchar_t * } } + +#endif diff --git a/src/libutil/windows/users.cc b/src/libutil/windows/users.cc index db6c42df381..90da0281f23 100644 --- a/src/libutil/windows/users.cc +++ b/src/libutil/windows/users.cc @@ -1,9 +1,10 @@ -#include "util.hh" -#include "users.hh" -#include "environment-variables.hh" -#include "file-system.hh" -#include "windows-error.hh" +#include "nix/util/util.hh" +#include "nix/util/users.hh" +#include "nix/util/environment-variables.hh" +#include "nix/util/file-system.hh" +#include "nix/util/windows-error.hh" +#ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include @@ -50,3 +51,4 @@ bool isRootUser() { } } +#endif diff --git a/src/libutil/windows/windows-async-pipe.cc b/src/libutil/windows/windows-async-pipe.cc index 4fa57ca3672..d47930a1b84 100644 --- a/src/libutil/windows/windows-async-pipe.cc +++ b/src/libutil/windows/windows-async-pipe.cc @@ -1,5 +1,7 @@ -#include "windows-async-pipe.hh" -#include "windows-error.hh" +#include "nix/util/windows-async-pipe.hh" +#include "nix/util/windows-error.hh" + +#ifdef _WIN32 namespace nix::windows { @@ -47,3 +49,5 @@ void AsyncPipe::close() } } + +#endif diff --git a/src/libutil/windows/windows-error.cc b/src/libutil/windows/windows-error.cc index aead4af2378..1e7aff830cd 100644 --- a/src/libutil/windows/windows-error.cc +++ b/src/libutil/windows/windows-error.cc @@ -1,5 +1,6 @@ -#include "windows-error.hh" +#include "nix/util/windows-error.hh" +#ifdef _WIN32 #include #define WIN32_LEAN_AND_MEAN #include @@ -29,3 +30,4 @@ std::string WinError::renderError(DWORD lastError) } } +#endif diff --git a/src/libutil/xml-writer.cc b/src/libutil/xml-writer.cc index 7993bee9af0..e460dd169cb 100644 --- a/src/libutil/xml-writer.cc +++ b/src/libutil/xml-writer.cc @@ -1,6 +1,6 @@ #include -#include "xml-writer.hh" +#include "nix/util/xml-writer.hh" namespace nix { diff --git a/src/nix-build/nix-build.cc b/src/nix-build/nix-build.cc index de01e1afcde..3977aaffe51 100644 --- a/src/nix-build/nix-build.cc +++ b/src/nix-build/nix-build.cc @@ -9,24 +9,26 @@ #include -#include "current-process.hh" -#include "parsed-derivations.hh" -#include "store-api.hh" -#include "local-fs-store.hh" -#include "globals.hh" -#include "realisation.hh" -#include "derivations.hh" -#include "shared.hh" -#include "path-with-outputs.hh" -#include "eval.hh" -#include "eval-inline.hh" -#include "get-drvs.hh" -#include "common-eval-args.hh" -#include "attr-path.hh" -#include "legacy.hh" -#include "users.hh" -#include "network-proxy.hh" -#include "compatibility-settings.hh" +#include "nix/util/current-process.hh" +#include "nix/store/parsed-derivations.hh" +#include "nix/store/derivation-options.hh" +#include "nix/store/store-open.hh" +#include "nix/store/local-fs-store.hh" +#include "nix/store/globals.hh" +#include "nix/store/realisation.hh" +#include "nix/store/derivations.hh" +#include "nix/main/shared.hh" +#include "nix/store/path-with-outputs.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/expr/get-drvs.hh" +#include "nix/cmd/common-eval-args.hh" +#include "nix/expr/attr-path.hh" +#include "nix/cmd/legacy.hh" +#include "nix/util/users.hh" +#include "nix/cmd/network-proxy.hh" +#include "nix/cmd/compatibility-settings.hh" +#include "man-pages.hh" using namespace nix; using namespace std::string_literals; @@ -145,7 +147,7 @@ static void main_nix_build(int argc, char * * argv) std::string outLink = "./result"; // List of environment variables kept for --pure - std::set keepVars{ + StringSet keepVars{ "HOME", "XDG_RUNTIME_DIR", "USER", "LOGNAME", "DISPLAY", "WAYLAND_DISPLAY", "WAYLAND_SOCKET", "PATH", "TERM", "IN_NIX_SHELL", "NIX_SHELL_PRESERVE_PROMPT", "TZ", "PAGER", "NIX_BUILD_SHELL", "SHLVL", @@ -253,16 +255,16 @@ static void main_nix_build(int argc, char * * argv) std::ostringstream joined; for (const auto & i : savedArgs) - joined << shellEscape(i) << ' '; + joined << escapeShellArgAlways(i) << ' '; if (std::regex_search(interpreter, std::regex("ruby"))) { // Hack for Ruby. Ruby also examines the shebang. It tries to // read the shebang to understand which packages to read from. Since // this is handled via nix-shell -p, we wrap our ruby script execution // in ruby -e 'load' which ignores the shebangs. - envCommand = fmt("exec %1% %2% -e 'load(ARGV.shift)' -- %3% %4%", execArgs, interpreter, shellEscape(script), toView(joined)); + envCommand = fmt("exec %1% %2% -e 'load(ARGV.shift)' -- %3% %4%", execArgs, interpreter, escapeShellArgAlways(script), toView(joined)); } else { - envCommand = fmt("exec %1% %2% %3% %4%", execArgs, interpreter, shellEscape(script), toView(joined)); + envCommand = fmt("exec %1% %2% %3% %4%", execArgs, interpreter, escapeShellArgAlways(script), toView(joined)); } } @@ -385,8 +387,8 @@ static void main_nix_build(int argc, char * * argv) return false; } bool add = false; - if (v.type() == nFunction && v.payload.lambda.fun->hasFormals()) { - for (auto & i : v.payload.lambda.fun->formals->formals) { + if (v.type() == nFunction && v.lambda().fun->hasFormals()) { + for (auto & i : v.lambda().fun->formals->formals) { if (state->symbols[i.name] == "inNixShell") { add = true; break; @@ -418,15 +420,8 @@ static void main_nix_build(int argc, char * * argv) state->maybePrintStats(); auto buildPaths = [&](const std::vector & paths) { - /* Note: we do this even when !printMissing to efficiently - fetch binary cache data. */ - uint64_t downloadSize, narSize; - StorePathSet willBuild, willSubstitute, unknown; - store->queryMissing(paths, - willBuild, willSubstitute, unknown, downloadSize, narSize); - if (settings.printMissing) - printMissing(ref(store), willBuild, willSubstitute, unknown, downloadSize, narSize); + printMissing(ref(store), paths); if (!dryRun) store->buildPaths(paths, buildMode, evalStore); @@ -472,7 +467,7 @@ static void main_nix_build(int argc, char * * argv) } catch (Error & e) { logError(e.info()); - notice("will use bash from your environment"); + notice("uses bash from your environment"); shell = "bash"; } } @@ -540,14 +535,23 @@ static void main_nix_build(int argc, char * * argv) env["NIX_BUILD_TOP"] = env["TMPDIR"] = env["TEMPDIR"] = env["TMP"] = env["TEMP"] = tmpDir.path().string(); env["NIX_STORE"] = store->storeDir; - env["NIX_BUILD_CORES"] = std::to_string(settings.buildCores); + env["NIX_BUILD_CORES"] = fmt("%d", settings.buildCores ? settings.buildCores : settings.getDefaultCores()); - auto passAsFile = tokenizeString(getOr(drv.env, "passAsFile", "")); + auto parsedDrv = StructuredAttrs::tryParse(drv.env); + DerivationOptions drvOptions; + try { + drvOptions = DerivationOptions::fromStructuredAttrs( + drv.env, + parsedDrv ? &*parsedDrv : nullptr); + } catch (Error & e) { + e.addTrace({}, "while parsing derivation '%s'", store->printStorePath(packageInfo.requireDrvPath())); + throw; + } int fileNr = 0; for (auto & var : drv.env) - if (passAsFile.count(var.first)) { + if (drvOptions.passAsFile.count(var.first)) { auto fn = ".attr-" + std::to_string(fileNr++); Path p = (tmpDir.path() / fn).string(); writeFile(p, var.second); @@ -557,7 +561,7 @@ static void main_nix_build(int argc, char * * argv) std::string structuredAttrsRC; - if (env.count("__json")) { + if (parsedDrv) { StorePathSet inputs; std::function::ChildNode &)> accumInputClosure; @@ -575,21 +579,22 @@ static void main_nix_build(int argc, char * * argv) for (const auto & [inputDrv, inputNode] : drv.inputDrvs.map) accumInputClosure(inputDrv, inputNode); - ParsedDerivation parsedDrv(packageInfo.requireDrvPath(), drv); + auto json = parsedDrv->prepareStructuredAttrs( + *store, + drvOptions, + inputs, + drv.outputs); - if (auto structAttrs = parsedDrv.prepareStructuredAttrs(*store, inputs)) { - auto json = structAttrs.value(); - structuredAttrsRC = writeStructuredAttrsShell(json); + structuredAttrsRC = StructuredAttrs::writeShell(json); - auto attrsJSON = (tmpDir.path() / ".attrs.json").string(); - writeFile(attrsJSON, json.dump()); + auto attrsJSON = (tmpDir.path() / ".attrs.json").string(); + writeFile(attrsJSON, json.dump()); - auto attrsSH = (tmpDir.path() / ".attrs.sh").string(); - writeFile(attrsSH, structuredAttrsRC); + auto attrsSH = (tmpDir.path() / ".attrs.sh").string(); + writeFile(attrsSH, structuredAttrsRC); - env["NIX_ATTRS_SH_FILE"] = attrsSH; - env["NIX_ATTRS_JSON_FILE"] = attrsJSON; - } + env["NIX_ATTRS_SH_FILE"] = attrsSH; + env["NIX_ATTRS_JSON_FILE"] = attrsJSON; } /* Run a shell using the derivation's environment. For @@ -625,12 +630,12 @@ static void main_nix_build(int argc, char * * argv) "unset TZ; %6%" "shopt -s execfail;" "%7%", - shellEscape(tmpDir.path().string()), + escapeShellArgAlways(tmpDir.path().string()), (pure ? "" : "p=$PATH; "), (pure ? "" : "PATH=$PATH:$p; unset p; "), - shellEscape(dirOf(*shell)), - shellEscape(*shell), - (getenv("TZ") ? (std::string("export TZ=") + shellEscape(getenv("TZ")) + "; ") : ""), + escapeShellArgAlways(dirOf(*shell)), + escapeShellArgAlways(*shell), + (getenv("TZ") ? (std::string("export TZ=") + escapeShellArgAlways(getenv("TZ")) + "; ") : ""), envCommand); vomit("Sourcing nix-shell with file %s and contents:\n%s", rcfile, rc); writeFile(rcfile, rc); diff --git a/src/nix-channel/nix-channel.cc b/src/nix-channel/nix-channel.cc index 56d1d7abb77..c4a05865823 100644 --- a/src/nix-channel/nix-channel.cc +++ b/src/nix-channel/nix-channel.cc @@ -1,13 +1,16 @@ -#include "profiles.hh" -#include "shared.hh" -#include "globals.hh" -#include "filetransfer.hh" -#include "store-api.hh" -#include "legacy.hh" -#include "eval-settings.hh" // for defexpr -#include "users.hh" -#include "tarball.hh" +#include "nix/store/profiles.hh" +#include "nix/main/shared.hh" +#include "nix/store/globals.hh" +#include "nix/store/filetransfer.hh" +#include "nix/store/store-open.hh" +#include "nix/cmd/legacy.hh" +#include "nix/cmd/common-eval-args.hh" +#include "nix/expr/eval-settings.hh" // for defexpr +#include "nix/util/users.hh" +#include "nix/fetchers/tarball.hh" +#include "nix/fetchers/fetch-settings.hh" #include "self-exe.hh" +#include "man-pages.hh" #include #include @@ -15,7 +18,7 @@ using namespace nix; -typedef std::map Channels; +typedef StringMap Channels; static Channels channels; static std::filesystem::path channelsList; @@ -113,7 +116,7 @@ static void update(const StringSet & channelNames) // We want to download the url to a file to see if it's a tarball while also checking if we // got redirected in the process, so that we can grab the various parts of a nix channel // definition from a consistent location if the redirect changes mid-download. - auto result = fetchers::downloadFile(store, url, std::string(baseNameOf(url))); + auto result = fetchers::downloadFile(store, fetchSettings, url, std::string(baseNameOf(url))); auto filename = store->toRealPath(result.storePath); url = result.effectiveUrl; @@ -127,9 +130,9 @@ static void update(const StringSet & channelNames) if (!unpacked) { // Download the channel tarball. try { - filename = store->toRealPath(fetchers::downloadFile(store, url + "/nixexprs.tar.xz", "nixexprs.tar.xz").storePath); + filename = store->toRealPath(fetchers::downloadFile(store, fetchSettings, url + "/nixexprs.tar.xz", "nixexprs.tar.xz").storePath); } catch (FileTransferError & e) { - filename = store->toRealPath(fetchers::downloadFile(store, url + "/nixexprs.tar.bz2", "nixexprs.tar.bz2").storePath); + filename = store->toRealPath(fetchers::downloadFile(store, fetchSettings, url + "/nixexprs.tar.bz2", "nixexprs.tar.bz2").storePath); } } // Regardless of where it came from, add the expression representing this channel to accumulated expression diff --git a/src/nix-channel/unpack-channel.nix b/src/nix-channel/unpack-channel.nix index 10515bc8b91..84e324a4d89 100644 --- a/src/nix-channel/unpack-channel.nix +++ b/src/nix-channel/unpack-channel.nix @@ -1,4 +1,8 @@ -{ name, channelName, src }: +{ + name, + channelName, + src, +}: derivation { builder = "builtin:unpack-channel"; diff --git a/src/nix-collect-garbage/nix-collect-garbage.cc b/src/nix-collect-garbage/nix-collect-garbage.cc index 20d5161df09..7f86b2b5cca 100644 --- a/src/nix-collect-garbage/nix-collect-garbage.cc +++ b/src/nix-collect-garbage/nix-collect-garbage.cc @@ -1,12 +1,13 @@ -#include "file-system.hh" -#include "signals.hh" -#include "store-api.hh" -#include "store-cast.hh" -#include "gc-store.hh" -#include "profiles.hh" -#include "shared.hh" -#include "globals.hh" -#include "legacy.hh" +#include "nix/util/file-system.hh" +#include "nix/util/signals.hh" +#include "nix/store/store-open.hh" +#include "nix/store/store-cast.hh" +#include "nix/store/gc-store.hh" +#include "nix/store/profiles.hh" +#include "nix/main/shared.hh" +#include "nix/store/globals.hh" +#include "nix/cmd/legacy.hh" +#include "man-pages.hh" #include #include @@ -23,23 +24,23 @@ bool dryRun = false; * Of course, this makes rollbacks to before this point in time * impossible. */ -void removeOldGenerations(fs::path dir) +void removeOldGenerations(std::filesystem::path dir) { if (access(dir.string().c_str(), R_OK) != 0) return; bool canWrite = access(dir.string().c_str(), W_OK) == 0; - for (auto & i : fs::directory_iterator{dir}) { + for (auto & i : DirectoryIterator{dir}) { checkInterrupt(); auto path = i.path().string(); auto type = i.symlink_status().type(); - if (type == fs::file_type::symlink && canWrite) { + if (type == std::filesystem::file_type::symlink && canWrite) { std::string link; try { link = readLink(path); - } catch (fs::filesystem_error & e) { + } catch (std::filesystem::filesystem_error & e) { if (e.code() == std::errc::no_such_file_or_directory) continue; throw; } @@ -51,7 +52,7 @@ void removeOldGenerations(fs::path dir) } else deleteOldGenerations(path, dryRun); } - } else if (type == fs::file_type::directory) { + } else if (type == std::filesystem::file_type::directory) { removeOldGenerations(path); } } @@ -83,10 +84,10 @@ static int main_nix_collect_garbage(int argc, char * * argv) }); if (removeOld) { - std::set dirsToClean = { + std::set dirsToClean = { profilesDir(), - fs::path{settings.nixStateDir} / "profiles", - fs::path{getDefaultProfile()}.parent_path(), + std::filesystem::path{settings.nixStateDir} / "profiles", + std::filesystem::path{getDefaultProfile()}.parent_path(), }; for (auto & dir : dirsToClean) removeOldGenerations(dir); diff --git a/src/nix-copy-closure/nix-copy-closure.cc b/src/nix-copy-closure/nix-copy-closure.cc index b64af758fcb..87d0f65905b 100644 --- a/src/nix-copy-closure/nix-copy-closure.cc +++ b/src/nix-copy-closure/nix-copy-closure.cc @@ -1,7 +1,8 @@ -#include "shared.hh" -#include "realisation.hh" -#include "store-api.hh" -#include "legacy.hh" +#include "nix/main/shared.hh" +#include "nix/store/realisation.hh" +#include "nix/store/store-open.hh" +#include "nix/cmd/legacy.hh" +#include "man-pages.hh" using namespace nix; diff --git a/src/nix-env/buildenv.nix b/src/nix-env/buildenv.nix index 0bac4c44b48..c8955a94e99 100644 --- a/src/nix-env/buildenv.nix +++ b/src/nix-env/buildenv.nix @@ -8,13 +8,15 @@ derivation { inherit manifest; # !!! grmbl, need structured data for passing this in a clean way. - derivations = - map (d: - [ (d.meta.active or "true") - (d.meta.priority or 5) - (builtins.length d.outputs) - ] ++ map (output: builtins.getAttr output d) d.outputs) - derivations; + derivations = map ( + d: + [ + (d.meta.active or "true") + (d.meta.priority or 5) + (builtins.length d.outputs) + ] + ++ map (output: builtins.getAttr output d) d.outputs + ) derivations; # Building user environments remotely just causes huge amounts of # network traffic, so don't do that. diff --git a/src/nix-env/nix-env.cc b/src/nix-env/nix-env.cc index e9eb5270895..fd48e67dce4 100644 --- a/src/nix-env/nix-env.cc +++ b/src/nix-env/nix-env.cc @@ -1,22 +1,23 @@ -#include "users.hh" -#include "attr-path.hh" -#include "common-eval-args.hh" -#include "derivations.hh" -#include "eval.hh" -#include "get-drvs.hh" -#include "globals.hh" -#include "names.hh" -#include "profiles.hh" -#include "path-with-outputs.hh" -#include "shared.hh" -#include "store-api.hh" -#include "local-fs-store.hh" +#include "nix/util/users.hh" +#include "nix/expr/attr-path.hh" +#include "nix/cmd/common-eval-args.hh" +#include "nix/store/derivations.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/get-drvs.hh" +#include "nix/store/globals.hh" +#include "nix/store/names.hh" +#include "nix/store/profiles.hh" +#include "nix/store/path-with-outputs.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-open.hh" +#include "nix/store/local-fs-store.hh" #include "user-env.hh" -#include "value-to-json.hh" -#include "xml-writer.hh" -#include "legacy.hh" -#include "eval-settings.hh" // for defexpr -#include "terminal.hh" +#include "nix/expr/value-to-json.hh" +#include "nix/util/xml-writer.hh" +#include "nix/cmd/legacy.hh" +#include "nix/expr/eval-settings.hh" // for defexpr +#include "nix/util/terminal.hh" +#include "man-pages.hh" #include #include @@ -237,9 +238,9 @@ static void checkSelectorUse(DrvNames & selectors) namespace { -std::set searchByPrefix(const PackageInfos & allElems, std::string_view prefix) { +StringSet searchByPrefix(const PackageInfos & allElems, std::string_view prefix) { constexpr std::size_t maxResults = 3; - std::set result; + StringSet result; for (const auto & packageInfo : allElems) { const auto drvName = DrvName { packageInfo.queryName() }; if (hasPrefix(drvName.name, prefix)) { @@ -501,9 +502,17 @@ static bool keep(PackageInfo & drv) return drv.queryMetaBool("keep", false); } +static void setMetaFlag(EvalState & state, PackageInfo & drv, + const std::string & name, const std::string & value) +{ + auto v = state.allocValue(); + v->mkString(value); + drv.setMeta(name, v); +} + static void installDerivations(Globals & globals, - const Strings & args, const Path & profile) + const Strings & args, const Path & profile, std::optional priority) { debug("installing derivations"); @@ -527,6 +536,11 @@ static void installDerivations(Globals & globals, newNames.insert(DrvName(i.queryName()).name); } + if (priority) { + for (auto & drv : newElems) { + setMetaFlag(*globals.state, drv, "priority", std::to_string((priority.value()))); + } + } while (true) { auto lockToken = optimisticLockProfile(profile); @@ -564,6 +578,7 @@ static void installDerivations(Globals & globals, static void opInstall(Globals & globals, Strings opFlags, Strings opArgs) { + std::optional priority; for (Strings::iterator i = opFlags.begin(); i != opFlags.end(); ) { auto arg = *i++; if (parseInstallSourceOptions(globals, i, opFlags, arg)) ; @@ -571,10 +586,17 @@ static void opInstall(Globals & globals, Strings opFlags, Strings opArgs) globals.preserveInstalled = true; else if (arg == "--remove-all" || arg == "-r") globals.removeAll = true; + else if (arg == "--priority") { + if (i == opFlags.end()) + throw UsageError("'%1%' requires an argument", arg); + priority = string2Int(*i++); + if (!priority) + throw UsageError("'--priority' requires an integer argument"); + } else throw UsageError("unknown flag '%1%'", arg); } - installDerivations(globals, opArgs, globals.profile); + installDerivations(globals, opArgs, globals.profile, priority); } @@ -689,15 +711,6 @@ static void opUpgrade(Globals & globals, Strings opFlags, Strings opArgs) } -static void setMetaFlag(EvalState & state, PackageInfo & drv, - const std::string & name, const std::string & value) -{ - auto v = state.allocValue(); - v->mkString(value); - drv.setMeta(name, v); -} - - static void opSetFlag(Globals & globals, Strings opFlags, Strings opArgs) { if (opFlags.size() > 0) @@ -1252,7 +1265,7 @@ static void opQuery(Globals & globals, Strings opFlags, Strings opArgs) } else if (v->type() == nList) { attrs2["type"] = "strings"; XMLOpenElement m(xml, "meta", attrs2); - for (auto elem : v->listItems()) { + for (auto elem : v->listView()) { if (elem->type() != nString) continue; XMLAttrs attrs3; attrs3["value"] = elem->c_str(); @@ -1507,7 +1520,8 @@ static int main_nix_env(int argc, char * * argv) opFlags.push_back(*arg); /* FIXME: hacky */ if (*arg == "--from-profile" || - (op == opQuery && (*arg == "--attr" || *arg == "-A"))) + (op == opQuery && (*arg == "--attr" || *arg == "-A")) || + (op == opInstall && (*arg == "--priority"))) opFlags.push_back(getArg(*arg, arg, end)); } else diff --git a/src/nix-env/user-env.cc b/src/nix-env/user-env.cc index ee62077c0a7..e149b6aeb7f 100644 --- a/src/nix-env/user-env.cc +++ b/src/nix-env/user-env.cc @@ -1,14 +1,14 @@ #include "user-env.hh" -#include "derivations.hh" -#include "store-api.hh" -#include "path-with-outputs.hh" -#include "local-fs-store.hh" -#include "globals.hh" -#include "shared.hh" -#include "eval.hh" -#include "eval-inline.hh" -#include "profiles.hh" -#include "print-ambiguous.hh" +#include "nix/store/derivations.hh" +#include "nix/store/store-api.hh" +#include "nix/store/path-with-outputs.hh" +#include "nix/store/local-fs-store.hh" +#include "nix/store/globals.hh" +#include "nix/main/shared.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/store/profiles.hh" +#include "nix/expr/print-ambiguous.hh" #include #include diff --git a/src/nix-env/user-env.hh b/src/nix-env/user-env.hh index 15da3fcb3f0..0a19b8f3214 100644 --- a/src/nix-env/user-env.hh +++ b/src/nix-env/user-env.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "get-drvs.hh" +#include "nix/expr/get-drvs.hh" namespace nix { diff --git a/src/nix-instantiate/nix-instantiate.cc b/src/nix-instantiate/nix-instantiate.cc index c4854951124..f7b218efce4 100644 --- a/src/nix-instantiate/nix-instantiate.cc +++ b/src/nix-instantiate/nix-instantiate.cc @@ -1,17 +1,18 @@ -#include "globals.hh" -#include "print-ambiguous.hh" -#include "shared.hh" -#include "eval.hh" -#include "eval-inline.hh" -#include "get-drvs.hh" -#include "attr-path.hh" -#include "signals.hh" -#include "value-to-xml.hh" -#include "value-to-json.hh" -#include "store-api.hh" -#include "local-fs-store.hh" -#include "common-eval-args.hh" -#include "legacy.hh" +#include "nix/store/globals.hh" +#include "nix/expr/print-ambiguous.hh" +#include "nix/main/shared.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/expr/get-drvs.hh" +#include "nix/expr/attr-path.hh" +#include "nix/util/signals.hh" +#include "nix/expr/value-to-xml.hh" +#include "nix/expr/value-to-json.hh" +#include "nix/store/store-open.hh" +#include "nix/store/local-fs-store.hh" +#include "nix/cmd/common-eval-args.hh" +#include "nix/cmd/legacy.hh" +#include "man-pages.hh" #include #include @@ -24,7 +25,7 @@ static Path gcRoot; static int rootNr = 0; -enum OutputKind { okPlain, okXML, okJSON }; +enum OutputKind { okPlain, okRaw, okXML, okJSON }; void processExpr(EvalState & state, const Strings & attrPaths, bool parseOnly, bool strict, Bindings & autoArgs, @@ -50,7 +51,11 @@ void processExpr(EvalState & state, const Strings & attrPaths, vRes = v; else state.autoCallFunction(autoArgs, v, vRes); - if (output == okXML) + if (output == okRaw) + std::cout << *state.coerceToString(noPos, vRes, context, "while generating the nix-instantiate output"); + // We intentionally don't output a newline here. The default PS1 for Bash in NixOS starts with a newline + // and other interactive shells like Zsh are smart enough to print a missing newline before the prompt. + else if (output == okXML) printValueAsXML(state, strict, location, vRes, std::cout, context, noPos); else if (output == okJSON) { printValueAsJSON(state, strict, vRes, v.determinePos(noPos), std::cout, context); @@ -132,6 +137,8 @@ static int main_nix_instantiate(int argc, char * * argv) gcRoot = getArg(*arg, arg, end); else if (*arg == "--indirect") ; + else if (*arg == "--raw") + outputKind = okRaw; else if (*arg == "--xml") outputKind = okXML; else if (*arg == "--json") diff --git a/src/nix-store/dotgraph.cc b/src/nix-store/dotgraph.cc index 2c530999b55..f8054b554c2 100644 --- a/src/nix-store/dotgraph.cc +++ b/src/nix-store/dotgraph.cc @@ -1,5 +1,5 @@ #include "dotgraph.hh" -#include "store-api.hh" +#include "nix/store/store-api.hh" #include diff --git a/src/nix-store/dotgraph.hh b/src/nix-store/dotgraph.hh index 4fd9440803f..b8e0721ab6f 100644 --- a/src/nix-store/dotgraph.hh +++ b/src/nix-store/dotgraph.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "store-api.hh" +#include "nix/store/store-api.hh" namespace nix { diff --git a/src/nix-store/graphml.cc b/src/nix-store/graphml.cc index 3e789a2d8b3..3b3188a4126 100644 --- a/src/nix-store/graphml.cc +++ b/src/nix-store/graphml.cc @@ -1,6 +1,6 @@ #include "graphml.hh" -#include "store-api.hh" -#include "derivations.hh" +#include "nix/store/store-api.hh" +#include "nix/store/derivations.hh" #include diff --git a/src/nix-store/graphml.hh b/src/nix-store/graphml.hh index bd3a4a37c46..afcedb58eff 100644 --- a/src/nix-store/graphml.hh +++ b/src/nix-store/graphml.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "store-api.hh" +#include "nix/store/store-api.hh" namespace nix { diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc index b731b25afed..3da7a8ac108 100644 --- a/src/nix-store/nix-store.cc +++ b/src/nix-store/nix-store.cc @@ -1,22 +1,24 @@ -#include "archive.hh" -#include "derivations.hh" +#include "nix/util/archive.hh" +#include "nix/store/derivations.hh" #include "dotgraph.hh" -#include "globals.hh" -#include "store-cast.hh" -#include "local-fs-store.hh" -#include "log-store.hh" -#include "serve-protocol.hh" -#include "serve-protocol-connection.hh" -#include "shared.hh" +#include "nix/store/globals.hh" +#include "nix/store/store-open.hh" +#include "nix/store/store-cast.hh" +#include "nix/store/local-fs-store.hh" +#include "nix/store/log-store.hh" +#include "nix/store/serve-protocol.hh" +#include "nix/store/serve-protocol-connection.hh" +#include "nix/main/shared.hh" #include "graphml.hh" -#include "legacy.hh" -#include "posix-source-accessor.hh" -#include "path-with-outputs.hh" +#include "nix/cmd/legacy.hh" +#include "nix/util/posix-source-accessor.hh" +#include "nix/store/path-with-outputs.hh" +#include "man-pages.hh" #ifndef _WIN32 // TODO implement on Windows or provide allowed-to-noop interface -# include "local-store.hh" -# include "monitor-fd.hh" -# include "posix-fs-canonicalise.hh" +# include "nix/store/local-store.hh" +# include "nix/util/monitor-fd.hh" +# include "nix/store/posix-fs-canonicalise.hh" #endif #include @@ -26,9 +28,9 @@ #include #include -#include "build-result.hh" -#include "exit.hh" -#include "serve-protocol-impl.hh" +#include "nix/store/build-result.hh" +#include "nix/util/exit.hh" +#include "nix/store/serve-protocol-impl.hh" namespace nix_store { @@ -144,23 +146,19 @@ static void opRealise(Strings opFlags, Strings opArgs) for (auto & i : opArgs) paths.push_back(followLinksToStorePathWithOutputs(*store, i)); - uint64_t downloadSize, narSize; - StorePathSet willBuild, willSubstitute, unknown; - store->queryMissing( - toDerivedPaths(paths), - willBuild, willSubstitute, unknown, downloadSize, narSize); + auto missing = store->queryMissing(toDerivedPaths(paths)); /* Filter out unknown paths from `paths`. */ if (ignoreUnknown) { std::vector paths2; for (auto & i : paths) - if (!unknown.count(i.path)) paths2.push_back(i); + if (!missing.unknown.count(i.path)) paths2.push_back(i); paths = std::move(paths2); - unknown = StorePathSet(); + missing.unknown = StorePathSet(); } if (settings.printMissing) - printMissing(ref(store), willBuild, willSubstitute, unknown, downloadSize, narSize); + printMissing(ref(store), missing); if (dryRun) return; @@ -183,9 +181,9 @@ static void opAdd(Strings opFlags, Strings opArgs) if (!opFlags.empty()) throw UsageError("unknown flag"); for (auto & i : opArgs) { - auto [accessor, canonPath] = PosixSourceAccessor::createAtRoot(i); + auto sourcePath = PosixSourceAccessor::createAtRoot(makeParentCanonical(i)); cout << fmt("%s\n", store->printStorePath(store->addToStore( - std::string(baseNameOf(i)), {accessor, canonPath}))); + std::string(baseNameOf(i)), sourcePath))); } } @@ -207,10 +205,10 @@ static void opAddFixed(Strings opFlags, Strings opArgs) opArgs.pop_front(); for (auto & i : opArgs) { - auto [accessor, canonPath] = PosixSourceAccessor::createAtRoot(i); + auto sourcePath = PosixSourceAccessor::createAtRoot(makeParentCanonical(i)); std::cout << fmt("%s\n", store->printStorePath(store->addToStoreSlow( baseNameOf(i), - {accessor, canonPath}, + sourcePath, method, hashAlgo).path)); } @@ -252,7 +250,7 @@ static StorePathSet maybeUseOutputs(const StorePath & storePath, bool useOutput, return store->queryDerivationOutputs(storePath); for (auto & i : drv.outputsAndOptPaths(*store)) { if (!i.second.second) - throw UsageError("Cannot use output path of floating content-addressed derivation until we know what it is (e.g. by building it)"); + throw UsageError("Cannot use output path of floating content-addressing derivation until we know what it is (e.g. by building it)"); outputs.insert(*i.second.second); } return outputs; @@ -496,7 +494,7 @@ static void opPrintEnv(Strings opFlags, Strings opArgs) /* Print each environment variable in the derivation in a format * that can be sourced by the shell. */ for (auto & i : drv.env) - logger->cout("export %1%; %1%=%2%\n", i.first, shellEscape(i.second)); + logger->cout("export %1%; %1%=%2%\n", i.first, escapeShellArgAlways(i.second)); /* Also output the arguments. This doesn't preserve whitespace in arguments. */ @@ -505,7 +503,7 @@ static void opPrintEnv(Strings opFlags, Strings opArgs) for (auto & i : drv.args) { if (!first) cout << ' '; first = false; - cout << shellEscape(i); + cout << escapeShellArgAlways(i); } cout << "'\n"; } @@ -562,7 +560,7 @@ static void registerValidity(bool reregister, bool hashGiven, bool canonicalise) #endif if (!hashGiven) { HashResult hash = hashPath( - {store->getFSAccessor(false), CanonPath { store->printStorePath(info->path) }}, + {store->getFSAccessor(false), CanonPath { info->path.to_string() }}, FileSerialisationMethod::NixArchive, HashAlgorithm::SHA256); info->narHash = hash.first; info->narSize = hash.second; @@ -860,7 +858,7 @@ static void opServe(Strings opFlags, Strings opArgs) auto options = ServeProto::Serialise::read(*store, rconn); - // Only certain feilds get initialized based on the protocol + // Only certain fields get initialized based on the protocol // version. This is why not all the code below is unconditional. // See how the serialization logic in // `ServeProto::Serialise` matches diff --git a/src/nix/add-to-store.cc b/src/nix/add-to-store.cc index 5c08f761662..9b7306fdd5d 100644 --- a/src/nix/add-to-store.cc +++ b/src/nix/add-to-store.cc @@ -1,10 +1,10 @@ -#include "command.hh" -#include "common-args.hh" -#include "store-api.hh" -#include "archive.hh" -#include "git.hh" -#include "posix-source-accessor.hh" -#include "misc-store-flags.hh" +#include "nix/cmd/command.hh" +#include "nix/main/common-args.hh" +#include "nix/store/store-api.hh" +#include "nix/util/archive.hh" +#include "nix/util/git.hh" +#include "nix/util/posix-source-accessor.hh" +#include "nix/cmd/misc-store-flags.hh" using namespace nix; @@ -37,13 +37,13 @@ struct CmdAddToStore : MixDryRun, StoreCommand { if (!namePart) namePart = baseNameOf(path); - auto [accessor, path2] = PosixSourceAccessor::createAtRoot(path); + auto sourcePath = PosixSourceAccessor::createAtRoot(makeParentCanonical(path)); auto storePath = dryRun ? store->computeStorePath( - *namePart, {accessor, path2}, caMethod, hashAlgo, {}).first + *namePart, sourcePath, caMethod, hashAlgo, {}).first : store->addToStoreSlow( - *namePart, {accessor, path2}, caMethod, hashAlgo, {}).path; + *namePart, sourcePath, caMethod, hashAlgo, {}).path; logger->cout("%s", store->printStorePath(storePath)); } diff --git a/src/nix/app.cc b/src/nix/app.cc index 935ed18ecba..c9a9f9caf7d 100644 --- a/src/nix/app.cc +++ b/src/nix/app.cc @@ -1,13 +1,13 @@ -#include "installables.hh" -#include "installable-derived-path.hh" -#include "installable-value.hh" -#include "store-api.hh" -#include "eval-inline.hh" -#include "eval-cache.hh" -#include "names.hh" -#include "command.hh" -#include "derivations.hh" -#include "downstream-placeholder.hh" +#include "nix/cmd/installables.hh" +#include "nix/cmd/installable-derived-path.hh" +#include "nix/cmd/installable-value.hh" +#include "nix/store/store-api.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/expr/eval-cache.hh" +#include "nix/store/names.hh" +#include "nix/cmd/command.hh" +#include "nix/store/derivations.hh" +#include "nix/store/downstream-placeholder.hh" namespace nix { @@ -129,18 +129,23 @@ UnresolvedApp InstallableValue::toApp(EvalState & state) throw Error("attribute '%s' has unsupported type '%s'", cursor->getAttrPathStr(), type); } -// FIXME: move to libcmd -App UnresolvedApp::resolve(ref evalStore, ref store) +std::vector UnresolvedApp::build(ref evalStore, ref store) { - auto res = unresolved; - Installables installableContext; for (auto & ctxElt : unresolved.context) installableContext.push_back( make_ref(store, DerivedPath { ctxElt })); - auto builtContext = Installable::build(evalStore, store, Realise::Outputs, installableContext); + return Installable::build(evalStore, store, Realise::Outputs, installableContext); +} + +// FIXME: move to libcmd +App UnresolvedApp::resolve(ref evalStore, ref store) +{ + auto res = unresolved; + + auto builtContext = build(evalStore, store); res.program = resolveString(*store, unresolved.program, builtContext); if (!store->isInStore(res.program)) throw Error("app program '%s' is not in the Nix store", res.program); diff --git a/src/nix/build-utils-meson b/src/nix/build-utils-meson deleted file mode 120000 index 91937f18372..00000000000 --- a/src/nix/build-utils-meson +++ /dev/null @@ -1 +0,0 @@ -../../build-utils-meson/ \ No newline at end of file diff --git a/src/nix/build.cc b/src/nix/build.cc index 3569b0cde2a..bd0c8862b23 100644 --- a/src/nix/build.cc +++ b/src/nix/build.cc @@ -1,9 +1,8 @@ -#include "command.hh" -#include "common-args.hh" -#include "shared.hh" -#include "store-api.hh" -#include "local-fs-store.hh" -#include "progress-bar.hh" +#include "nix/cmd/command.hh" +#include "nix/main/common-args.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-api.hh" +#include "nix/store/local-fs-store.hh" #include @@ -42,29 +41,13 @@ static nlohmann::json builtPathsWithResultToJSON(const std::vectorcout("%s", derivedPathsToJSON(pathsToBuild, *store).dump()); + printJSON(derivedPathsToJSON(pathsToBuild, *store)); return; } @@ -115,12 +98,10 @@ struct CmdBuild : InstallablesCommand, MixDryRun, MixJSON, MixProfile if (json) logger->cout("%s", builtPathsWithResultToJSON(buildables, *store).dump()); - if (outLink != "") - if (auto store2 = store.dynamic_pointer_cast()) - createOutLinks(outLink, toBuiltPaths(buildables), *store2); + createOutLinksMaybe(buildables, store); if (printOutputPaths) { - stopProgressBar(); + logger->stop(); for (auto & buildable : buildables) { std::visit(overloaded { [&](const BuiltPath::Opaque & bo) { diff --git a/src/nix/bundle.cc b/src/nix/bundle.cc index 5b7862c4e0c..50d7bf6a34d 100644 --- a/src/nix/bundle.cc +++ b/src/nix/bundle.cc @@ -1,10 +1,10 @@ -#include "installable-flake.hh" -#include "command-installable-value.hh" -#include "common-args.hh" -#include "shared.hh" -#include "store-api.hh" -#include "local-fs-store.hh" -#include "eval-inline.hh" +#include "nix/cmd/installable-flake.hh" +#include "nix/cmd/command-installable-value.hh" +#include "nix/main/common-args.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-api.hh" +#include "nix/store/local-fs-store.hh" +#include "nix/expr/eval-inline.hh" namespace nix::fs { using namespace std::filesystem; } @@ -24,7 +24,7 @@ struct CmdBundle : InstallableValueCommand .handler = {&bundler}, .completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) { completeFlakeRef(completions, getStore(), prefix); - }} + }}, }); addFlag({ @@ -33,7 +33,7 @@ struct CmdBundle : InstallableValueCommand .description = "Override the name of the symlink to the build result. It defaults to the base name of the app.", .labels = {"path"}, .handler = {&outLink}, - .completer = completePath + .completer = completePath, }); } @@ -80,7 +80,7 @@ struct CmdBundle : InstallableValueCommand auto [bundlerFlakeRef, bundlerName, extendedOutputsSpec] = parseFlakeRefWithFragmentAndExtendedOutputsSpec( - fetchSettings, bundler, fs::current_path().string()); + fetchSettings, bundler, std::filesystem::current_path().string()); const flake::LockFlags lockFlags{ .writeLockFile = false }; InstallableFlake bundler{this, evalState, std::move(bundlerFlakeRef), bundlerName, std::move(extendedOutputsSpec), diff --git a/src/nix/cat.cc b/src/nix/cat.cc index e0179c3486c..aa27446d2bc 100644 --- a/src/nix/cat.cc +++ b/src/nix/cat.cc @@ -1,27 +1,26 @@ -#include "command.hh" -#include "store-api.hh" -#include "nar-accessor.hh" -#include "progress-bar.hh" +#include "nix/cmd/command.hh" +#include "nix/store/store-api.hh" +#include "nix/store/nar-accessor.hh" using namespace nix; struct MixCat : virtual Args { - std::string path; - - void cat(ref accessor) + void cat(ref accessor, CanonPath path) { - auto st = accessor->lstat(CanonPath(path)); + auto st = accessor->lstat(path); if (st.type != SourceAccessor::Type::tRegular) - throw Error("path '%1%' is not a regular file", path); - stopProgressBar(); + throw Error("path '%1%' is not a regular file", path.abs()); + logger->stop(); - writeFull(getStandardOutput(), accessor->readFile(CanonPath(path))); + writeFull(getStandardOutput(), accessor->readFile(path)); } }; struct CmdCatStore : StoreCommand, MixCat { + std::string path; + CmdCatStore() { expectArgs({ @@ -45,7 +44,8 @@ struct CmdCatStore : StoreCommand, MixCat void run(ref store) override { - cat(store->getFSAccessor()); + auto [storePath, rest] = store->toStorePath(path); + cat(store->getFSAccessor(), CanonPath{storePath.to_string()} / CanonPath{rest}); } }; @@ -53,6 +53,8 @@ struct CmdCatNar : StoreCommand, MixCat { Path narPath; + std::string path; + CmdCatNar() { expectArgs({ @@ -77,7 +79,7 @@ struct CmdCatNar : StoreCommand, MixCat void run(ref store) override { - cat(makeNarAccessor(readFile(narPath))); + cat(makeNarAccessor(readFile(narPath)), CanonPath{path}); } }; diff --git a/src/nix/config-check.cc b/src/nix/config-check.cc index a72b0654232..27d053b9f68 100644 --- a/src/nix/config-check.cc +++ b/src/nix/config-check.cc @@ -1,14 +1,14 @@ #include -#include "command.hh" -#include "exit.hh" -#include "logging.hh" -#include "serve-protocol.hh" -#include "shared.hh" -#include "store-api.hh" -#include "local-fs-store.hh" -#include "worker-protocol.hh" -#include "executable-path.hh" +#include "nix/cmd/command.hh" +#include "nix/util/exit.hh" +#include "nix/util/logging.hh" +#include "nix/store/serve-protocol.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-api.hh" +#include "nix/store/local-fs-store.hh" +#include "nix/store/worker-protocol.hh" +#include "nix/util/executable-path.hh" namespace nix::fs { using namespace std::filesystem; } @@ -78,12 +78,12 @@ struct CmdConfigCheck : StoreCommand bool checkNixInPath() { - std::set dirs; + std::set dirs; for (auto & dir : ExecutablePath::load().directories) { auto candidate = dir / "nix-env"; - if (fs::exists(candidate)) - dirs.insert(fs::canonical(candidate).parent_path() ); + if (std::filesystem::exists(candidate)) + dirs.insert(std::filesystem::canonical(candidate).parent_path() ); } if (dirs.size() != 1) { @@ -99,12 +99,12 @@ struct CmdConfigCheck : StoreCommand bool checkProfileRoots(ref store) { - std::set dirs; + std::set dirs; for (auto & dir : ExecutablePath::load().directories) { auto profileDir = dir.parent_path(); try { - auto userEnv = fs::weakly_canonical(profileDir); + auto userEnv = std::filesystem::weakly_canonical(profileDir); auto noContainsProfiles = [&]{ for (auto && part : profileDir) @@ -114,8 +114,8 @@ struct CmdConfigCheck : StoreCommand if (store->isStorePath(userEnv.string()) && hasSuffix(userEnv.string(), "user-environment")) { while (noContainsProfiles() && std::filesystem::is_symlink(profileDir)) - profileDir = fs::weakly_canonical( - profileDir.parent_path() / fs::read_symlink(profileDir)); + profileDir = std::filesystem::weakly_canonical( + profileDir.parent_path() / std::filesystem::read_symlink(profileDir)); if (noContainsProfiles()) dirs.insert(dir); diff --git a/src/nix/config.cc b/src/nix/config.cc index 07f975a006a..cd82b08a6a1 100644 --- a/src/nix/config.cc +++ b/src/nix/config.cc @@ -1,8 +1,8 @@ -#include "command.hh" -#include "common-args.hh" -#include "shared.hh" -#include "store-api.hh" -#include "config-global.hh" +#include "nix/cmd/command.hh" +#include "nix/main/common-args.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-api.hh" +#include "nix/util/config-global.hh" #include @@ -63,7 +63,7 @@ struct CmdConfigShow : Command, MixJSON if (json) { // FIXME: use appropriate JSON types (bool, ints, etc). - logger->cout("%s", globalConfig.toJSON().dump()); + printJSON(globalConfig.toJSON()); } else { logger->cout("%s", globalConfig.toKeyValue()); } diff --git a/src/nix/copy.cc b/src/nix/copy.cc index 399a6c0fd34..013f2a7e393 100644 --- a/src/nix/copy.cc +++ b/src/nix/copy.cc @@ -1,7 +1,7 @@ -#include "command.hh" -#include "shared.hh" -#include "store-api.hh" -#include "local-fs-store.hh" +#include "nix/cmd/command.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-api.hh" +#include "nix/store/local-fs-store.hh" using namespace nix; @@ -21,7 +21,7 @@ struct CmdCopy : virtual CopyCommand, virtual BuiltPathsCommand, MixProfile .description = "Create symlinks prefixed with *path* to the top-level store paths fetched from the source store.", .labels = {"path"}, .handler = {&outLink}, - .completer = completePath + .completer = completePath, }); addFlag({ diff --git a/src/nix/crash-handler.cc b/src/nix/crash-handler.cc new file mode 100644 index 00000000000..d65773fa0d5 --- /dev/null +++ b/src/nix/crash-handler.cc @@ -0,0 +1,68 @@ +#include "crash-handler.hh" + +#include "nix/util/fmt.hh" +#include "nix/util/logging.hh" + +#include +#include +#include + +// Darwin and FreeBSD stdenv do not define _GNU_SOURCE but do have _Unwind_Backtrace. +#if defined(__APPLE__) || defined(__FreeBSD__) +# define BOOST_STACKTRACE_GNU_SOURCE_NOT_REQUIRED +#endif + +#include + +#ifndef _WIN32 +# include +#endif + +namespace nix { + +namespace { + +void logFatal(std::string const & s) +{ + writeToStderr(s + "\n"); + // std::string for guaranteed null termination +#ifndef _WIN32 + syslog(LOG_CRIT, "%s", s.c_str()); +#endif +} + +void onTerminate() +{ + logFatal( + "Nix crashed. This is a bug. Please report this at https://github.com/NixOS/nix/issues with the following information included:\n"); + try { + std::exception_ptr eptr = std::current_exception(); + if (eptr) { + std::rethrow_exception(eptr); + } else { + logFatal("std::terminate() called without exception"); + } + } catch (const std::exception & ex) { + logFatal(fmt("Exception: %s: %s", boost::core::demangle(typeid(ex).name()), ex.what())); + } catch (...) { + logFatal("Unknown exception!"); + } + + logFatal("Stack trace:"); + std::stringstream ss; + ss << boost::stacktrace::stacktrace(); + logFatal(ss.str()); + + std::abort(); +} +} + +void registerCrashHandler() +{ + // DO NOT use this for signals. Boost stacktrace is very much not + // async-signal-safe, and in a world with ASLR, addr2line is pointless. + // + // If you want signals, set up a minidump system and do it out-of-process. + std::set_terminate(onTerminate); +} +} diff --git a/src/nix/crash-handler.hh b/src/nix/crash-handler.hh new file mode 100644 index 00000000000..018e867474e --- /dev/null +++ b/src/nix/crash-handler.hh @@ -0,0 +1,11 @@ +#pragma once +/// @file Crash handler for Nix that prints back traces (hopefully in instances where it is not just going to crash the +/// process itself). + +namespace nix { + +/** Registers the Nix crash handler for std::terminate (currently; will support more crashes later). See also + * detectStackOverflow(). */ +void registerCrashHandler(); + +} diff --git a/src/nix/derivation-add.cc b/src/nix/derivation-add.cc index 4d91d453800..e99c44deb2d 100644 --- a/src/nix/derivation-add.cc +++ b/src/nix/derivation-add.cc @@ -1,10 +1,10 @@ // FIXME: rename to 'nix plan add' or 'nix derivation add'? -#include "command.hh" -#include "common-args.hh" -#include "store-api.hh" -#include "archive.hh" -#include "derivations.hh" +#include "nix/cmd/command.hh" +#include "nix/main/common-args.hh" +#include "nix/store/store-api.hh" +#include "nix/util/archive.hh" +#include "nix/store/derivations.hh" #include using namespace nix; diff --git a/src/nix/derivation-show.cc b/src/nix/derivation-show.cc index bf637246d83..26108b8b8bf 100644 --- a/src/nix/derivation-show.cc +++ b/src/nix/derivation-show.cc @@ -1,17 +1,17 @@ -// FIXME: integrate this with nix path-info? -// FIXME: rename to 'nix store derivation show' or 'nix debug derivation show'? +// FIXME: integrate this with `nix path-info`? +// FIXME: rename to 'nix store derivation show'? -#include "command.hh" -#include "common-args.hh" -#include "store-api.hh" -#include "archive.hh" -#include "derivations.hh" +#include "nix/cmd/command.hh" +#include "nix/main/common-args.hh" +#include "nix/store/store-api.hh" +#include "nix/util/archive.hh" +#include "nix/store/derivations.hh" #include using namespace nix; using json = nlohmann::json; -struct CmdShowDerivation : InstallablesCommand +struct CmdShowDerivation : InstallablesCommand, MixPrintJSON { bool recursive = false; @@ -21,7 +21,7 @@ struct CmdShowDerivation : InstallablesCommand .longName = "recursive", .shortName = 'r', .description = "Include the dependencies of the specified derivations.", - .handler = {&recursive, true} + .handler = {&recursive, true}, }); } @@ -57,7 +57,7 @@ struct CmdShowDerivation : InstallablesCommand jsonRoot[store->printStorePath(drvPath)] = store->readDerivation(drvPath).toJSON(*store); } - logger->cout(jsonRoot.dump(2)); + printJSON(jsonRoot); } }; diff --git a/src/nix/derivation.cc b/src/nix/derivation.cc index 59a78d37879..ee62ab4dc69 100644 --- a/src/nix/derivation.cc +++ b/src/nix/derivation.cc @@ -1,4 +1,4 @@ -#include "command.hh" +#include "nix/cmd/command.hh" using namespace nix; diff --git a/src/nix/develop.cc b/src/nix/develop.cc index 1736add9abd..37bce6ca078 100644 --- a/src/nix/develop.cc +++ b/src/nix/develop.cc @@ -1,13 +1,12 @@ -#include "config-global.hh" -#include "eval.hh" -#include "installable-flake.hh" -#include "command-installable-value.hh" -#include "common-args.hh" -#include "shared.hh" -#include "store-api.hh" -#include "outputs-spec.hh" -#include "derivations.hh" -#include "progress-bar.hh" +#include "nix/util/config-global.hh" +#include "nix/expr/eval.hh" +#include "nix/cmd/installable-flake.hh" +#include "nix/cmd/command-installable-value.hh" +#include "nix/main/common-args.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-api.hh" +#include "nix/store/outputs-spec.hh" +#include "nix/store/derivations.hh" #ifndef _WIN32 // TODO re-enable on Windows # include "run.hh" @@ -19,7 +18,7 @@ #include #include -#include "strings.hh" +#include "nix/util/strings.hh" namespace nix::fs { using namespace std::filesystem; } @@ -56,21 +55,19 @@ struct BuildEnvironment using Array = std::vector; - using Associative = std::map; + using Associative = StringMap; using Value = std::variant; std::map vars; - std::map bashFunctions; + StringMap bashFunctions; std::optional> structuredAttrs; - static BuildEnvironment fromJSON(std::string_view in) + static BuildEnvironment fromJSON(const nlohmann::json & json) { BuildEnvironment res; - std::set exported; - - auto json = nlohmann::json::parse(in); + StringSet exported; for (auto & [name, info] : json["variables"].items()) { std::string type = info["type"]; @@ -93,7 +90,14 @@ struct BuildEnvironment return res; } - std::string toJSON() const + static BuildEnvironment parseJSON(std::string_view in) + { + auto json = nlohmann::json::parse(in); + + return fromJSON(json); + } + + nlohmann::json toJSON() const { auto res = nlohmann::json::object(); @@ -125,11 +129,9 @@ struct BuildEnvironment res["structuredAttrs"] = std::move(contents); } - auto json = res.dump(); + assert(BuildEnvironment::fromJSON(res) == *this); - assert(BuildEnvironment::fromJSON(json) == *this); - - return json; + return res; } bool providesStructuredAttrs() const @@ -149,25 +151,25 @@ struct BuildEnvironment return structuredAttrs->second; } - void toBash(std::ostream & out, const std::set & ignoreVars) const + void toBash(std::ostream & out, const StringSet & ignoreVars) const { for (auto & [name, value] : vars) { if (!ignoreVars.count(name)) { if (auto str = std::get_if(&value)) { - out << fmt("%s=%s\n", name, shellEscape(str->value)); + out << fmt("%s=%s\n", name, escapeShellArgAlways(str->value)); if (str->exported) out << fmt("export %s\n", name); } else if (auto arr = std::get_if(&value)) { out << "declare -a " << name << "=("; for (auto & s : *arr) - out << shellEscape(s) << " "; + out << escapeShellArgAlways(s) << " "; out << ")\n"; } else if (auto arr = std::get_if(&value)) { out << "declare -A " << name << "=("; for (auto & [n, v] : *arr) - out << "[" << shellEscape(n) << "]=" << shellEscape(v) << " "; + out << "[" << escapeShellArgAlways(n) << "]=" << escapeShellArgAlways(v) << " "; out << ")\n"; } } @@ -306,7 +308,7 @@ static StorePath getDerivationEnvironment(ref store, ref evalStore struct Common : InstallableCommand, MixProfile { - std::set ignoreVars{ + StringSet ignoreVars{ "BASHOPTS", "HOME", // FIXME: don't ignore in pure mode? "NIX_BUILD_TOP", @@ -335,7 +337,7 @@ struct Common : InstallableCommand, MixProfile .labels = {"installable", "outputs-dir"}, .handler = {[&](std::string installable, std::string outputsDir) { redirects.push_back({installable, outputsDir}); - }} + }}, }); } @@ -343,7 +345,7 @@ struct Common : InstallableCommand, MixProfile ref store, const BuildEnvironment & buildEnvironment, const std::filesystem::path & tmpDir, - const std::filesystem::path & outputsDir = fs::path { fs::current_path() } / "outputs") + const std::filesystem::path & outputsDir = std::filesystem::path { std::filesystem::current_path() } / "outputs") { // A list of colon-separated environment variables that should be // prepended to, rather than overwritten, in order to keep the shell usable. @@ -506,7 +508,7 @@ struct Common : InstallableCommand, MixProfile debug("reading environment file '%s'", strPath); - return {BuildEnvironment::fromJSON(readFile(store->toRealPath(shellOutPath))), strPath}; + return {BuildEnvironment::parseJSON(readFile(store->toRealPath(shellOutPath))), strPath}; } }; @@ -525,7 +527,7 @@ struct CmdDevelop : Common, MixEnvironment .handler = {[&](std::vector ss) { if (ss.empty()) throw UsageError("--command requires at least one argument"); command = ss; - }} + }}, }); addFlag({ @@ -612,7 +614,7 @@ struct CmdDevelop : Common, MixEnvironment std::vector args; args.reserve(command.size()); for (const auto & s : command) - args.push_back(shellEscape(s)); + args.push_back(escapeShellArgAlways(s)); script += fmt("exec %s\n", concatStringsSep(" ", args)); } @@ -620,13 +622,13 @@ struct CmdDevelop : Common, MixEnvironment script = "[ -n \"$PS1\" ] && [ -e ~/.bashrc ] && source ~/.bashrc;\nshopt -u expand_aliases\n" + script + "\nshopt -s expand_aliases\n"; if (developSettings.bashPrompt != "") script += fmt("[ -n \"$PS1\" ] && PS1=%s;\n", - shellEscape(developSettings.bashPrompt.get())); + escapeShellArgAlways(developSettings.bashPrompt.get())); if (developSettings.bashPromptPrefix != "") script += fmt("[ -n \"$PS1\" ] && PS1=%s\"$PS1\";\n", - shellEscape(developSettings.bashPromptPrefix.get())); + escapeShellArgAlways(developSettings.bashPromptPrefix.get())); if (developSettings.bashPromptSuffix != "") script += fmt("[ -n \"$PS1\" ] && PS1+=%s;\n", - shellEscape(developSettings.bashPromptSuffix.get())); + escapeShellArgAlways(developSettings.bashPromptSuffix.get())); } writeFull(rcFileFd.get(), script); @@ -696,7 +698,7 @@ struct CmdDevelop : Common, MixEnvironment auto sourcePath = installableFlake->getLockedFlake()->flake.resolvedRef.input.getSourcePath(); if (sourcePath) { if (chdir(sourcePath->c_str()) == -1) { - throw SysError("chdir to '%s' failed", *sourcePath); + throw SysError("chdir to %s failed", *sourcePath); } } } @@ -731,10 +733,10 @@ struct CmdPrintDevEnv : Common, MixJSON { auto buildEnvironment = getBuildEnvironment(store, installable).first; - stopProgressBar(); + logger->stop(); if (json) { - logger->writeToStdout(buildEnvironment.toJSON()); + printJSON(buildEnvironment.toJSON()); } else { AutoDelete tmpDir(createTempDir("", "nix-dev-env"), true); logger->writeToStdout(makeRcScript(store, buildEnvironment, tmpDir)); diff --git a/src/nix/diff-closures.cc b/src/nix/diff-closures.cc index 2bc7fe82b1b..ff9f9db4098 100644 --- a/src/nix/diff-closures.cc +++ b/src/nix/diff-closures.cc @@ -1,12 +1,12 @@ -#include "command.hh" -#include "shared.hh" -#include "store-api.hh" -#include "common-args.hh" -#include "names.hh" +#include "nix/cmd/command.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-api.hh" +#include "nix/main/common-args.hh" +#include "nix/store/names.hh" #include -#include "strings.hh" +#include "nix/util/strings.hh" namespace nix { @@ -47,10 +47,10 @@ GroupedPaths getClosureInfo(ref store, const StorePath & toplevel) return groupedPaths; } -std::string showVersions(const std::set & versions) +std::string showVersions(const StringSet & versions) { if (versions.empty()) return "∅"; - std::set versions2; + StringSet versions2; for (auto & version : versions) versions2.insert(version.empty() ? "ε" : version); return concatStringsSep(", ", versions2); @@ -65,7 +65,7 @@ void printClosureDiff( auto beforeClosure = getClosureInfo(store, beforePath); auto afterClosure = getClosureInfo(store, afterPath); - std::set allNames; + StringSet allNames; for (auto & [name, _] : beforeClosure) allNames.insert(name); for (auto & [name, _] : afterClosure) allNames.insert(name); @@ -87,11 +87,11 @@ void printClosureDiff( auto sizeDelta = (int64_t) afterSize - (int64_t) beforeSize; auto showDelta = std::abs(sizeDelta) >= 8 * 1024; - std::set removed, unchanged; + StringSet removed, unchanged; for (auto & [version, _] : beforeVersions) if (!afterVersions.count(version)) removed.insert(version); else unchanged.insert(version); - std::set added; + StringSet added; for (auto & [version, _] : afterVersions) if (!beforeVersions.count(version)) added.insert(version); diff --git a/src/nix/dump-path.cc b/src/nix/dump-path.cc index 98a059fa1bd..c883630b1fd 100644 --- a/src/nix/dump-path.cc +++ b/src/nix/dump-path.cc @@ -1,6 +1,6 @@ -#include "command.hh" -#include "store-api.hh" -#include "archive.hh" +#include "nix/cmd/command.hh" +#include "nix/store/store-api.hh" +#include "nix/util/archive.hh" using namespace nix; diff --git a/src/nix/edit.cc b/src/nix/edit.cc index 9cbab230b0f..cfb9eb74a87 100644 --- a/src/nix/edit.cc +++ b/src/nix/edit.cc @@ -1,10 +1,9 @@ -#include "current-process.hh" -#include "command-installable-value.hh" -#include "shared.hh" -#include "eval.hh" -#include "attr-path.hh" -#include "progress-bar.hh" -#include "editor-for.hh" +#include "nix/util/current-process.hh" +#include "nix/cmd/command-installable-value.hh" +#include "nix/main/shared.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/attr-path.hh" +#include "nix/cmd/editor-for.hh" #include @@ -40,7 +39,7 @@ struct CmdEdit : InstallableValueCommand } }(); - stopProgressBar(); + logger->stop(); auto args = editorFor(file, line); diff --git a/src/nix/env.cc b/src/nix/env.cc index 832320320ae..277bd0fdd45 100644 --- a/src/nix/env.cc +++ b/src/nix/env.cc @@ -1,11 +1,11 @@ #include #include -#include "command.hh" -#include "eval.hh" +#include "nix/cmd/command.hh" +#include "nix/expr/eval.hh" #include "run.hh" -#include "strings.hh" -#include "executable-path.hh" +#include "nix/util/strings.hh" +#include "nix/util/executable-path.hh" using namespace nix; @@ -38,16 +38,17 @@ struct CmdShell : InstallablesCommand, MixEnvironment CmdShell() { - addFlag( - {.longName = "command", - .shortName = 'c', - .description = "Command and arguments to be executed, defaulting to `$SHELL`", - .labels = {"command", "args"}, - .handler = {[&](std::vector ss) { - if (ss.empty()) - throw UsageError("--command requires at least one argument"); - command = ss; - }}}); + addFlag({ + .longName = "command", + .shortName = 'c', + .description = "Command and arguments to be executed, defaulting to `$SHELL`", + .labels = {"command", "args"}, + .handler = {[&](std::vector ss) { + if (ss.empty()) + throw UsageError("--command requires at least one argument"); + command = ss; + }}, + }); } std::string description() override @@ -64,11 +65,11 @@ struct CmdShell : InstallablesCommand, MixEnvironment void run(ref store, Installables && installables) override { + auto state = getEvalState(); + auto outPaths = Installable::toStorePaths(getEvalStore(), store, Realise::Outputs, OperateOn::Output, installables); - auto accessor = store->getFSAccessor(); - std::unordered_set done; std::queue todo; for (auto & path : outPaths) @@ -84,13 +85,16 @@ struct CmdShell : InstallablesCommand, MixEnvironment if (!done.insert(path).second) continue; - if (true) - pathAdditions.push_back(store->printStorePath(path) + "/bin"); + auto binDir = state->storeFS->resolveSymlinks(CanonPath(store->printStorePath(path)) / "bin"); + if (!store->isInStore(binDir.abs())) + throw Error("path '%s' is not in the Nix store", binDir); + + pathAdditions.push_back(binDir.abs()); - auto propPath = accessor->resolveSymlinks( + auto propPath = state->storeFS->resolveSymlinks( CanonPath(store->printStorePath(path)) / "nix-support" / "propagated-user-env-packages"); - if (auto st = accessor->maybeLstat(propPath); st && st->type == SourceAccessor::tRegular) { - for (auto & p : tokenizeString(accessor->readFile(propPath))) + if (auto st = state->storeFS->maybeLstat(propPath); st && st->type == SourceAccessor::tRegular) { + for (auto & p : tokenizeString(state->storeFS->readFile(propPath))) todo.push(store->parseStorePath(p)); } } @@ -107,7 +111,7 @@ struct CmdShell : InstallablesCommand, MixEnvironment // Release our references to eval caches to ensure they are persisted to disk, because // we are about to exec out of this process without running C++ destructors. - getEvalState()->evalCaches.clear(); + state->evalCaches.clear(); execProgramInStore(store, UseLookupPath::Use, *command.begin(), args); } diff --git a/src/nix/eval.cc b/src/nix/eval.cc index 7811b77edad..be064e5527a 100644 --- a/src/nix/eval.cc +++ b/src/nix/eval.cc @@ -1,11 +1,10 @@ -#include "command-installable-value.hh" -#include "common-args.hh" -#include "shared.hh" -#include "store-api.hh" -#include "eval.hh" -#include "eval-inline.hh" -#include "value-to-json.hh" -#include "progress-bar.hh" +#include "nix/cmd/command-installable-value.hh" +#include "nix/main/common-args.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-api.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/expr/value-to-json.hh" #include @@ -17,7 +16,7 @@ struct CmdEval : MixJSON, InstallableValueCommand, MixReadOnlyOption { bool raw = false; std::optional apply; - std::optional writeTo; + std::optional writeTo; CmdEval() : InstallableValueCommand() { @@ -75,21 +74,21 @@ struct CmdEval : MixJSON, InstallableValueCommand, MixReadOnlyOption } if (writeTo) { - stopProgressBar(); + logger->stop(); - if (fs::symlink_exists(*writeTo)) + if (pathExists(*writeTo)) throw Error("path '%s' already exists", writeTo->string()); - std::function recurse; + std::function recurse; - recurse = [&](Value & v, const PosIdx pos, const fs::path & path) + recurse = [&](Value & v, const PosIdx pos, const std::filesystem::path & path) { state->forceValue(v, pos); if (v.type() == nString) // FIXME: disallow strings with contexts? writeFile(path.string(), v.string_view()); else if (v.type() == nAttrs) { - [[maybe_unused]] bool directoryCreated = fs::create_directory(path); + [[maybe_unused]] bool directoryCreated = std::filesystem::create_directory(path); // Directory should not already exist assert(directoryCreated); for (auto & attr : *v.attrs()) { @@ -114,12 +113,12 @@ struct CmdEval : MixJSON, InstallableValueCommand, MixReadOnlyOption } else if (raw) { - stopProgressBar(); + logger->stop(); writeFull(getStandardOutput(), *state->coerceToString(noPos, *v, context, "while generating the eval command output")); } else if (json) { - logger->cout("%s", printValueAsJSON(*state, true, *v, pos, context, false)); + printJSON(printValueAsJSON(*state, true, *v, pos, context, false)); } else { diff --git a/src/nix/flake-command.hh b/src/nix/flake-command.hh new file mode 100644 index 00000000000..36dfe44c632 --- /dev/null +++ b/src/nix/flake-command.hh @@ -0,0 +1,27 @@ +#pragma once + +#include "nix/cmd/command.hh" +#include "nix/cmd/installable-flake.hh" +#include "nix/flake/flake.hh" + +namespace nix { + +using namespace nix::flake; + +class FlakeCommand : virtual Args, public MixFlakeOptions +{ +protected: + std::string flakeUrl = "."; + +public: + + FlakeCommand(); + + FlakeRef getFlakeRef(); + + LockedFlake lockFlake(); + + std::vector getFlakeRefsForCompletion() override; +}; + +} diff --git a/src/nix/flake-lock.md b/src/nix/flake-lock.md index d13666a4c0d..1148f5ef230 100644 --- a/src/nix/flake-lock.md +++ b/src/nix/flake-lock.md @@ -32,7 +32,7 @@ R""( This command updates the lock file of a flake (`flake.lock`) so that it contains an up-to-date lock for every flake input specified in -`flake.nix`. Lock file entries are aready up-to-date are not modified. +`flake.nix`. Lock file entries are already up-to-date are not modified. If you want to update existing lock entries, use [`nix flake update`](@docroot@/command-ref/new-cli/nix3-flake-update.md) diff --git a/src/nix/flake-prefetch.md b/src/nix/flake-prefetch.md index a1cf0289ae9..4666aadc4df 100644 --- a/src/nix/flake-prefetch.md +++ b/src/nix/flake-prefetch.md @@ -5,10 +5,14 @@ R""( * Download a tarball and unpack it: ```console - # nix flake prefetch https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.5.tar.xz + # nix flake prefetch https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.5.tar.xz --out-link ./result Downloaded 'https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.5.tar.xz?narHash=sha256-3XYHZANT6AFBV0BqegkAZHbba6oeDkIUCDwbATLMhAY=' to '/nix/store/sl5vvk8mb4ma1sjyy03kwpvkz50hd22d-source' (hash 'sha256-3XYHZANT6AFBV0BqegkAZHbba6oeDkIUCDwbATLMhAY='). + + # cat ./result/README + Linux kernel + … ``` * Download the `dwarffs` flake (looked up in the flake registry): diff --git a/src/nix/flake.cc b/src/nix/flake.cc index 9f3584a11ac..1d20add02ce 100644 --- a/src/nix/flake.cc +++ b/src/nix/flake.cc @@ -1,28 +1,28 @@ -#include "command.hh" -#include "installable-flake.hh" -#include "common-args.hh" -#include "shared.hh" -#include "eval.hh" -#include "eval-inline.hh" -#include "eval-settings.hh" -#include "flake/flake.hh" -#include "get-drvs.hh" -#include "signals.hh" -#include "store-api.hh" -#include "derivations.hh" -#include "outputs-spec.hh" -#include "attr-path.hh" -#include "fetchers.hh" -#include "registry.hh" -#include "eval-cache.hh" -#include "markdown.hh" -#include "users.hh" +#include "flake-command.hh" +#include "nix/main/common-args.hh" +#include "nix/main/shared.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/expr/eval-settings.hh" +#include "nix/expr/get-drvs.hh" +#include "nix/util/signals.hh" +#include "nix/store/store-open.hh" +#include "nix/store/derivations.hh" +#include "nix/store/outputs-spec.hh" +#include "nix/expr/attr-path.hh" +#include "nix/fetchers/fetchers.hh" +#include "nix/fetchers/registry.hh" +#include "nix/expr/eval-cache.hh" +#include "nix/cmd/markdown.hh" +#include "nix/util/users.hh" +#include "nix/fetchers/fetch-to-store.hh" +#include "nix/store/local-fs-store.hh" #include #include #include -#include "strings-inline.hh" +#include "nix/util/strings-inline.hh" namespace nix::fs { using namespace std::filesystem; } @@ -31,43 +31,36 @@ using namespace nix::flake; using json = nlohmann::json; struct CmdFlakeUpdate; -class FlakeCommand : virtual Args, public MixFlakeOptions -{ -protected: - std::string flakeUrl = "."; - -public: - FlakeCommand() - { - expectArgs({ - .label = "flake-url", - .optional = true, - .handler = {&flakeUrl}, - .completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) { - completeFlakeRef(completions, getStore(), prefix); - }} - }); - } +FlakeCommand::FlakeCommand() +{ + expectArgs({ + .label = "flake-url", + .optional = true, + .handler = {&flakeUrl}, + .completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) { + completeFlakeRef(completions, getStore(), prefix); + }} + }); +} - FlakeRef getFlakeRef() - { - return parseFlakeRef(fetchSettings, flakeUrl, fs::current_path().string()); //FIXME - } +FlakeRef FlakeCommand::getFlakeRef() +{ + return parseFlakeRef(fetchSettings, flakeUrl, std::filesystem::current_path().string()); //FIXME +} - LockedFlake lockFlake() - { - return flake::lockFlake(flakeSettings, *getEvalState(), getFlakeRef(), lockFlags); - } +LockedFlake FlakeCommand::lockFlake() +{ + return flake::lockFlake(flakeSettings, *getEvalState(), getFlakeRef(), lockFlags); +} - std::vector getFlakeRefsForCompletion() override - { - return { - // Like getFlakeRef but with expandTilde calld first - parseFlakeRef(fetchSettings, expandTilde(flakeUrl), fs::current_path().string()) - }; - } -}; +std::vector FlakeCommand::getFlakeRefsForCompletion() +{ + return { + // Like getFlakeRef but with expandTilde called first + parseFlakeRef(fetchSettings, expandTilde(flakeUrl), std::filesystem::current_path().string()) + }; +} struct CmdFlakeUpdate : FlakeCommand { @@ -88,28 +81,28 @@ struct CmdFlakeUpdate : FlakeCommand .handler={&flakeUrl}, .completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) { completeFlakeRef(completions, getStore(), prefix); - }} + }}, }); expectArgs({ .label="inputs", .optional=true, .handler={[&](std::vector inputsToUpdate){ for (const auto & inputToUpdate : inputsToUpdate) { - InputPath inputPath; + InputAttrPath inputAttrPath; try { - inputPath = flake::parseInputPath(inputToUpdate); + inputAttrPath = flake::parseInputAttrPath(inputToUpdate); } catch (Error & e) { warn("Invalid flake input '%s'. To update a specific flake, use 'nix flake update --flake %s' instead.", inputToUpdate, inputToUpdate); throw e; } - if (lockFlags.inputUpdates.contains(inputPath)) - warn("Input '%s' was specified multiple times. You may have done this by accident."); - lockFlags.inputUpdates.insert(inputPath); + if (lockFlags.inputUpdates.contains(inputAttrPath)) + warn("Input '%s' was specified multiple times. You may have done this by accident.", printInputAttrPath(inputAttrPath)); + lockFlags.inputUpdates.insert(inputAttrPath); } }}, .completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) { - completeFlakeInputPath(completions, getEvalState(), getFlakeRefsForCompletion(), prefix); - }} + completeFlakeInputAttrPath(completions, getEvalState(), getFlakeRefsForCompletion(), prefix); + }}, }); /* Remove flags that don't make sense. */ @@ -162,6 +155,7 @@ struct CmdFlakeLock : FlakeCommand settings.tarballTtl = 0; lockFlags.writeLockFile = true; + lockFlags.failOnUnlocked = true; lockFlags.applyNixConfig = true; lockFlake(); @@ -213,7 +207,7 @@ struct CmdFlakeMetadata : FlakeCommand, MixJSON auto & flake = lockedFlake.flake; // Currently, all flakes are in the Nix store via the rootFS accessor. - auto storePath = store->printStorePath(sourcePathToStorePath(store, flake.path).first); + auto storePath = store->printStorePath(store->toStorePath(flake.path.path.abs()).first); if (json) { nlohmann::json j; @@ -237,9 +231,9 @@ struct CmdFlakeMetadata : FlakeCommand, MixJSON j["lastModified"] = *lastModified; j["path"] = storePath; j["locks"] = lockedFlake.lockFile.toJSON().first; - if (auto fingerprint = lockedFlake.getFingerprint(store)) + if (auto fingerprint = lockedFlake.getFingerprint(store, fetchSettings)) j["fingerprint"] = fingerprint->to_string(HashFormat::Base16, false); - logger->cout("%s", j.dump()); + printJSON(j); } else { logger->cout( ANSI_BOLD "Resolved URL:" ANSI_NORMAL " %s", @@ -271,7 +265,7 @@ struct CmdFlakeMetadata : FlakeCommand, MixJSON logger->cout( ANSI_BOLD "Last modified:" ANSI_NORMAL " %s", std::put_time(std::localtime(&*lastModified), "%F %T")); - if (auto fingerprint = lockedFlake.getFingerprint(store)) + if (auto fingerprint = lockedFlake.getFingerprint(store, fetchSettings)) logger->cout( ANSI_BOLD "Fingerprint:" ANSI_NORMAL " %s", fingerprint->to_string(HashFormat::Base16, false)); @@ -303,7 +297,7 @@ struct CmdFlakeMetadata : FlakeCommand, MixJSON } else if (auto follows = std::get_if<1>(&input.second)) { logger->cout("%s" ANSI_BOLD "%s" ANSI_NORMAL " follows input '%s'", prefix + (last ? treeLast : treeConn), input.first, - printInputPath(*follows)); + printInputAttrPath(*follows)); } } }; @@ -333,12 +327,12 @@ struct CmdFlakeCheck : FlakeCommand addFlag({ .longName = "no-build", .description = "Do not build checks.", - .handler = {&build, false} + .handler = {&build, false}, }); addFlag({ .longName = "all-systems", .description = "Check the outputs for all systems.", - .handler = {&checkAllSystems, true} + .handler = {&checkAllSystems, true}, }); } @@ -383,7 +377,7 @@ struct CmdFlakeCheck : FlakeCommand } }; - std::set omittedSystems; + StringSet omittedSystems; // FIXME: rewrite to use EvalCache. @@ -489,8 +483,8 @@ struct CmdFlakeCheck : FlakeCommand if (!v.isLambda()) { throw Error("overlay is not a function, but %s instead", showType(v)); } - if (v.payload.lambda.fun->hasFormals() - || !argHasName(v.payload.lambda.fun->arg, "final")) + if (v.lambda().fun->hasFormals() + || !argHasName(v.lambda().fun->arg, "final")) throw Error("overlay does not take an argument named 'final'"); // FIXME: if we have a 'nixpkgs' input, use it to // evaluate the overlay. @@ -871,7 +865,7 @@ struct CmdFlakeInitCommon : virtual Args, EvalCommand defaultTemplateAttrPathsPrefixes, defaultTemplateAttrPaths, prefix); - }} + }}, }); } @@ -882,7 +876,7 @@ struct CmdFlakeInitCommon : virtual Args, EvalCommand auto evalState = getEvalState(); auto [templateFlakeRef, templateName] = parseFlakeRefWithFragment( - fetchSettings, templateUrl, fs::current_path().string()); + fetchSettings, templateUrl, std::filesystem::current_path().string()); auto installable = InstallableFlake(nullptr, evalState, std::move(templateFlakeRef), templateName, ExtendedOutputsSpec::Default(), @@ -896,25 +890,25 @@ struct CmdFlakeInitCommon : virtual Args, EvalCommand NixStringContext context; auto templateDir = evalState->coerceToPath(noPos, templateDirAttr, context, ""); - std::vector changedFiles; - std::vector conflictedFiles; + std::vector changedFiles; + std::vector conflictedFiles; - std::function copyDir; - copyDir = [&](const SourcePath & from, const fs::path & to) + std::function copyDir; + copyDir = [&](const SourcePath & from, const std::filesystem::path & to) { - fs::create_directories(to); + createDirs(to); for (auto & [name, entry] : from.readDirectory()) { checkInterrupt(); auto from2 = from / name; auto to2 = to / name; auto st = from2.lstat(); - auto to_st = fs::symlink_status(to2); + auto to_st = std::filesystem::symlink_status(to2); if (st.type == SourceAccessor::tDirectory) copyDir(from2, to2); else if (st.type == SourceAccessor::tRegular) { auto contents = from2.readFile(); - if (fs::exists(to_st)) { + if (std::filesystem::exists(to_st)) { auto contents2 = readFile(to2.string()); if (contents != contents2) { printError("refusing to overwrite existing file '%s'\n please merge it manually with '%s'", to2.string(), from2); @@ -928,8 +922,8 @@ struct CmdFlakeInitCommon : virtual Args, EvalCommand } else if (st.type == SourceAccessor::tSymlink) { auto target = from2.readLink(); - if (fs::exists(to_st)) { - if (fs::read_symlink(to2) != target) { + if (std::filesystem::exists(to_st)) { + if (std::filesystem::read_symlink(to2) != target) { printError("refusing to overwrite existing file '%s'\n please merge it manually with '%s'", to2.string(), from2); conflictedFiles.push_back(to2); } else { @@ -937,10 +931,10 @@ struct CmdFlakeInitCommon : virtual Args, EvalCommand } continue; } else - createSymlink(target, to2); + createSymlink(target, os_string_to_string(PathViewNG { to2 })); } else - throw Error("file '%s' has unsupported type", from2); + throw Error("path '%s' needs to be a symlink, file, or directory but instead is a %s", from2, st.typeString()); changedFiles.push_back(to2); notice("wrote: %s", to2); } @@ -948,7 +942,7 @@ struct CmdFlakeInitCommon : virtual Args, EvalCommand copyDir(templateDir, flakeDir); - if (!changedFiles.empty() && fs::exists(std::filesystem::path{flakeDir} / ".git")) { + if (!changedFiles.empty() && std::filesystem::exists(std::filesystem::path{flakeDir} / ".git")) { Strings args = { "-C", flakeDir, "add", "--intent-to-add", "--force", "--" }; for (auto & s : changedFiles) args.emplace_back(s.string()); runProgram("git", true, args); @@ -1031,7 +1025,7 @@ struct CmdFlakeClone : FlakeCommand .shortName = 'f', .description = "Clone the flake to path *dest*.", .labels = {"path"}, - .handler = {&destDir} + .handler = {&destDir}, }); } @@ -1048,13 +1042,22 @@ struct CmdFlakeArchive : FlakeCommand, MixJSON, MixDryRun { std::string dstUri; + CheckSigsFlag checkSigs = CheckSigs; + + SubstituteFlag substitute = NoSubstitute; + CmdFlakeArchive() { addFlag({ .longName = "to", .description = "URI of the destination Nix store", .labels = {"store-uri"}, - .handler = {&dstUri} + .handler = {&dstUri}, + }); + addFlag({ + .longName = "no-check-sigs", + .description = "Do not require that paths are signed by trusted keys.", + .handler = {&checkSigs, NoCheckSigs}, }); } @@ -1076,7 +1079,7 @@ struct CmdFlakeArchive : FlakeCommand, MixJSON, MixDryRun StorePathSet sources; - auto storePath = sourcePathToStorePath(store, flake.flake.path).first; + auto storePath = store->toStorePath(flake.flake.path.path.abs()).first; sources.insert(storePath); @@ -1087,19 +1090,21 @@ struct CmdFlakeArchive : FlakeCommand, MixJSON, MixDryRun nlohmann::json jsonObj2 = json ? json::object() : nlohmann::json(nullptr); for (auto & [inputName, input] : node.inputs) { if (auto inputNode = std::get_if<0>(&input)) { - auto storePath = - dryRun - ? (*inputNode)->lockedRef.input.computeStorePath(*store) - : (*inputNode)->lockedRef.input.fetchToStore(store).first; + std::optional storePath; + if (!(*inputNode)->lockedRef.input.isRelative()) { + storePath = + dryRun + ? (*inputNode)->lockedRef.input.computeStorePath(*store) + : (*inputNode)->lockedRef.input.fetchToStore(store).first; + sources.insert(*storePath); + } if (json) { - auto& jsonObj3 = jsonObj2[inputName]; - jsonObj3["path"] = store->printStorePath(storePath); - sources.insert(std::move(storePath)); + auto & jsonObj3 = jsonObj2[inputName]; + if (storePath) + jsonObj3["path"] = store->printStorePath(*storePath); jsonObj3["inputs"] = traverse(**inputNode); - } else { - sources.insert(std::move(storePath)); + } else traverse(**inputNode); - } } } return jsonObj2; @@ -1110,14 +1115,15 @@ struct CmdFlakeArchive : FlakeCommand, MixJSON, MixDryRun {"path", store->printStorePath(storePath)}, {"inputs", traverse(*flake.lockFile.root)}, }; - logger->cout("%s", jsonRoot); + printJSON(jsonRoot); } else { traverse(*flake.lockFile.root); } if (!dryRun && !dstUri.empty()) { ref dstStore = dstUri.empty() ? openStore() : openStore(dstUri); - copyPaths(*store, *dstStore, sources); + + copyPaths(*store, *dstStore, sources, NoRepair, checkSigs, substitute); } } }; @@ -1132,12 +1138,12 @@ struct CmdFlakeShow : FlakeCommand, MixJSON addFlag({ .longName = "legacy", .description = "Show the contents of the `legacyPackages` output.", - .handler = {&showLegacy, true} + .handler = {&showLegacy, true}, }); addFlag({ .longName = "all-systems", .description = "Show the contents of outputs for all systems.", - .handler = {&showAllSystems, true} + .handler = {&showAllSystems, true}, }); } @@ -1324,18 +1330,34 @@ struct CmdFlakeShow : FlakeCommand, MixJSON logger->warn(fmt("%s omitted (use '--all-systems' to show)", concatStringsSep(".", attrPathS))); } } else { - if (visitor.isDerivation()) - showDerivation(); - else - throw Error("expected a derivation"); + try { + if (visitor.isDerivation()) + showDerivation(); + else + throw Error("expected a derivation"); + } catch (IFDError & e) { + if (!json) { + logger->cout(fmt("%s " ANSI_WARNING "omitted due to use of import from derivation" ANSI_NORMAL, headerPrefix)); + } else { + logger->warn(fmt("%s omitted due to use of import from derivation", concatStringsSep(".", attrPathS))); + } + } } } else if (attrPath.size() > 0 && attrPathS[0] == "hydraJobs") { - if (visitor.isDerivation()) - showDerivation(); - else - recurse(); + try { + if (visitor.isDerivation()) + showDerivation(); + else + recurse(); + } catch (IFDError & e) { + if (!json) { + logger->cout(fmt("%s " ANSI_WARNING "omitted due to use of import from derivation" ANSI_NORMAL, headerPrefix)); + } else { + logger->warn(fmt("%s omitted due to use of import from derivation", concatStringsSep(".", attrPathS))); + } + } } else if (attrPath.size() > 0 && attrPathS[0] == "legacyPackages") { @@ -1354,11 +1376,19 @@ struct CmdFlakeShow : FlakeCommand, MixJSON logger->warn(fmt("%s omitted (use '--all-systems' to show)", concatStringsSep(".", attrPathS))); } } else { - if (visitor.isDerivation()) - showDerivation(); - else if (attrPath.size() <= 2) - // FIXME: handle recurseIntoAttrs - recurse(); + try { + if (visitor.isDerivation()) + showDerivation(); + else if (attrPath.size() <= 2) + // FIXME: handle recurseIntoAttrs + recurse(); + } catch (IFDError & e) { + if (!json) { + logger->cout(fmt("%s " ANSI_WARNING "omitted due to use of import from derivation" ANSI_NORMAL, headerPrefix)); + } else { + logger->warn(fmt("%s omitted due to use of import from derivation", concatStringsSep(".", attrPathS))); + } + } } } @@ -1422,14 +1452,24 @@ struct CmdFlakeShow : FlakeCommand, MixJSON auto j = visit(*cache->getRoot(), {}, fmt(ANSI_BOLD "%s" ANSI_NORMAL, flake->flake.lockedRef), ""); if (json) - logger->cout("%s", j.dump()); + printJSON(j); } }; struct CmdFlakePrefetch : FlakeCommand, MixJSON { + std::optional outLink; + CmdFlakePrefetch() { + addFlag({ + .longName = "out-link", + .shortName = 'o', + .description = "Create symlink named *path* to the resulting store path.", + .labels = {"path"}, + .handler = {&outLink}, + .completer = completePath, + }); } std::string description() override @@ -1448,7 +1488,8 @@ struct CmdFlakePrefetch : FlakeCommand, MixJSON { auto originalRef = getFlakeRef(); auto resolvedRef = originalRef.resolve(store); - auto [storePath, lockedRef] = resolvedRef.fetchTree(store); + auto [accessor, lockedRef] = resolvedRef.lazyFetch(store); + auto storePath = fetchToStore(getEvalState()->fetchSettings, *store, accessor, FetchMode::Copy, lockedRef.input.getName()); auto hash = store->queryPathInfo(storePath)->narHash; if (json) { @@ -1457,34 +1498,28 @@ struct CmdFlakePrefetch : FlakeCommand, MixJSON res["hash"] = hash.to_string(HashFormat::SRI, true); res["original"] = fetchers::attrsToJSON(resolvedRef.toAttrs()); res["locked"] = fetchers::attrsToJSON(lockedRef.toAttrs()); - logger->cout(res.dump()); + res["locked"].erase("__final"); // internal for now + printJSON(res); } else { notice("Downloaded '%s' to '%s' (hash '%s').", lockedRef.to_string(), store->printStorePath(storePath), hash.to_string(HashFormat::SRI, true)); } + + if (outLink) { + if (auto store2 = store.dynamic_pointer_cast()) + createOutLinks(*outLink, {BuiltPath::Opaque{storePath}}, *store2); + else + throw Error("'--out-link' is not supported for this Nix store"); + } } }; struct CmdFlake : NixMultiCommand { CmdFlake() - : NixMultiCommand( - "flake", - { - {"update", []() { return make_ref(); }}, - {"lock", []() { return make_ref(); }}, - {"metadata", []() { return make_ref(); }}, - {"info", []() { return make_ref(); }}, - {"check", []() { return make_ref(); }}, - {"init", []() { return make_ref(); }}, - {"new", []() { return make_ref(); }}, - {"clone", []() { return make_ref(); }}, - {"archive", []() { return make_ref(); }}, - {"show", []() { return make_ref(); }}, - {"prefetch", []() { return make_ref(); }}, - }) + : NixMultiCommand("flake", RegisterCommand::getCommandsFor({"flake"})) { } @@ -1508,3 +1543,14 @@ struct CmdFlake : NixMultiCommand }; static auto rCmdFlake = registerCommand("flake"); +static auto rCmdFlakeArchive = registerCommand2({"flake", "archive"}); +static auto rCmdFlakeCheck = registerCommand2({"flake", "check"}); +static auto rCmdFlakeClone = registerCommand2({"flake", "clone"}); +static auto rCmdFlakeInfo = registerCommand2({"flake", "info"}); +static auto rCmdFlakeInit = registerCommand2({"flake", "init"}); +static auto rCmdFlakeLock = registerCommand2({"flake", "lock"}); +static auto rCmdFlakeMetadata = registerCommand2({"flake", "metadata"}); +static auto rCmdFlakeNew = registerCommand2({"flake", "new"}); +static auto rCmdFlakePrefetch = registerCommand2({"flake", "prefetch"}); +static auto rCmdFlakeShow = registerCommand2({"flake", "show"}); +static auto rCmdFlakeUpdate = registerCommand2({"flake", "update"}); diff --git a/src/nix/flake.md b/src/nix/flake.md index 1e9895f6e44..6cb39fd5fd3 100644 --- a/src/nix/flake.md +++ b/src/nix/flake.md @@ -165,7 +165,8 @@ can occur in *locked* flake references and are available to Nix code: Currently the `type` attribute can be one of the following: -* `indirect`: *The default*. Indirection through the flake registry. +* `indirect`: *The default*. These are symbolic references to flakes + that are looked up in [the flake registries](./nix3-registry.md). These have the form ``` @@ -186,7 +187,7 @@ Currently the `type` attribute can be one of the following: * `nixpkgs/nixos-unstable/a3a3dda3bacf61e8a39258a0ed9c924eeca8e293` * `sub/dir` (if a flake named `sub` is in the registry) -* `path`: arbitrary local directories. The required attribute `path` +* `path`: arbitrary local directories. The required attribute `path` specifies the path of the flake. The URL form is ``` @@ -199,18 +200,38 @@ Currently the `type` attribute can be one of the following: If the flake at *path* is not inside a git repository, the `path:` prefix is implied and can be omitted. - *path* generally must be an absolute path. However, on the command - line, it can be a relative path (e.g. `.` or `./foo`) which is - interpreted as relative to the current directory. In this case, it - must start with `.` to avoid ambiguity with registry lookups - (e.g. `nixpkgs` is a registry lookup; `./nixpkgs` is a relative - path). + If *path* is a relative path (i.e. if it does not start with `/`), + it is interpreted as follows: + + - If *path* is a command line argument, it is interpreted relative + to the current directory. + + - If *path* is used in a `flake.nix`, it is interpreted relative to + the directory containing that `flake.nix`. However, the resolved + path must be in the same tree. For instance, a `flake.nix` in the + root of a tree can use `path:./foo` to access the flake in + subdirectory `foo`, but `path:../bar` is illegal. On the other + hand, a flake in the `/foo` directory of a tree can use + `path:../bar` to refer to the flake in `/bar`. + + Path inputs can be specified with path values in `flake.nix`. Path values are a syntax for `path` inputs, and they are converted by + 1. resolving them into relative paths, relative to the base directory of `flake.nix` + 2. escaping URL characters (refer to IETF RFC?) + 3. prepending `path:` + + Note that the allowed syntax for path values in flake `inputs` may be more restrictive than general Nix, so you may need to use `path:` if your path contains certain special characters. See [Path literals](@docroot@/language/syntax.md#path-literal) + + Note that if you omit `path:`, relative paths must start with `.` to + avoid ambiguity with registry lookups (e.g. `nixpkgs` is a registry + lookup; `./nixpkgs` is a relative path). For example, these are valid path flake references: * `path:/home/user/sub/dir` * `/home/user/sub/dir` (if `dir/flake.nix` is *not* in a git repository) - * `./sub/dir` (when used on the command line and `dir/flake.nix` is *not* in a git repository) + * `path:sub/dir` + * `./sub/dir` + * `path:../parent` * `git`: Git repositories. The location of the repository is specified by the attribute `url`. @@ -653,7 +674,7 @@ following fields: * `inputs`: The dependencies of this node, as a mapping from input names (e.g. `nixpkgs`) to node labels (e.g. `n2`). -* `original`: The original input specification from `flake.lock`, as a +* `original`: The original input specification from `flake.nix`, as a set of `builtins.fetchTree` arguments. * `locked`: The locked input specification, as a set of @@ -674,7 +695,7 @@ following fields: The attributes in `locked` are considered "final", meaning that they are the only ones that are passed via the arguments of the `outputs` function of a flake. For instance, if `locked` contains a `lastModified` attribute while the fetcher does not return a `lastModified` attribute, then the `lastModified` attribute will be passed to the `outputs` function. Conversely, if `locked` does *not* contain a `lastModified` attribute while the fetcher *does* return a `lastModified` attribute, then no `lastModified` attribute will be passed. - If `locked` contains a `lastModifed` attribute and the fetcher returns a `lastModified` attribute, then they must have the same value. + If `locked` contains a `lastModified` attribute and the fetcher returns a `lastModified` attribute, then they must have the same value. * `flake`: A Boolean denoting whether this is a flake or non-flake dependency. Corresponds to the `flake` attribute in the `inputs` diff --git a/src/nix/fmt.cc b/src/nix/fmt.cc deleted file mode 100644 index f444d6addf1..00000000000 --- a/src/nix/fmt.cc +++ /dev/null @@ -1,55 +0,0 @@ -#include "command.hh" -#include "installable-value.hh" -#include "eval.hh" -#include "run.hh" - -using namespace nix; - -struct CmdFmt : SourceExprCommand { - std::vector args; - - CmdFmt() { expectArgs({.label = "args", .handler = {&args}}); } - - std::string description() override { - return "reformat your code in the standard style"; - } - - std::string doc() override { - return - #include "fmt.md" - ; - } - - Category category() override { return catSecondary; } - - Strings getDefaultFlakeAttrPaths() override { - return Strings{"formatter." + settings.thisSystem.get()}; - } - - Strings getDefaultFlakeAttrPathPrefixes() override { return Strings{}; } - - void run(ref store) override - { - auto evalState = getEvalState(); - auto evalStore = getEvalStore(); - - auto installable_ = parseInstallable(store, "."); - auto & installable = InstallableValue::require(*installable_); - auto app = installable.toApp(*evalState).resolve(evalStore, store); - - Strings programArgs{app.program}; - - // Propagate arguments from the CLI - for (auto &i : args) { - programArgs.push_back(i); - } - - // Release our references to eval caches to ensure they are persisted to disk, because - // we are about to exec out of this process without running C++ destructors. - evalState->evalCaches.clear(); - - execProgramInStore(store, UseLookupPath::DontUse, app.program, programArgs); - }; -}; - -static auto r2 = registerCommand("fmt"); diff --git a/src/nix/fmt.md b/src/nix/fmt.md deleted file mode 100644 index b4693eb65bf..00000000000 --- a/src/nix/fmt.md +++ /dev/null @@ -1,47 +0,0 @@ -R""( - -# Description - -`nix fmt` calls the formatter specified in the flake. - -Flags can be forwarded to the formatter by using `--` followed by the flags. - -Any arguments will be forwarded to the formatter. Typically these are the files to format. - - -# Examples - -With [nixpkgs-fmt](https://github.com/nix-community/nixpkgs-fmt): - -```nix -# flake.nix -{ - outputs = { nixpkgs, self }: { - formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixpkgs-fmt; - }; -} -``` - -With [nixfmt](https://github.com/NixOS/nixfmt): - -```nix -# flake.nix -{ - outputs = { nixpkgs, self }: { - formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt-rfc-style; - }; -} -``` - -With [Alejandra](https://github.com/kamadorueda/alejandra): - -```nix -# flake.nix -{ - outputs = { nixpkgs, self }: { - formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.alejandra; - }; -} -``` - -)"" diff --git a/src/nix/formatter-build.md b/src/nix/formatter-build.md new file mode 100644 index 00000000000..fbb6adb1cec --- /dev/null +++ b/src/nix/formatter-build.md @@ -0,0 +1,23 @@ +R""( + +# Description + +`nix formatter build` builds the formatter specified in the flake. + +Similar to [`nix build`](@docroot@/command-ref/new-cli/nix3-build.md), +unless `--no-link` is specified, after a successful +build, it creates a symlink to the store path of the formatter. This symlink is +named `./result` by default; this can be overridden using the +`--out-link` option. + +It always prints the command to standard output. + +# Examples + +* Build the formatter: + + ```console + # nix formatter build + /nix/store/cb9w44vkhk2x4adfxwgdkkf5gjmm856j-treefmt/bin/treefmt + ``` +)"" diff --git a/src/nix/formatter-run.md b/src/nix/formatter-run.md new file mode 100644 index 00000000000..201cae92e4a --- /dev/null +++ b/src/nix/formatter-run.md @@ -0,0 +1,29 @@ +R""( + +# Description + +`nix fmt` (an alias for `nix formatter run`) calls the formatter specified in the flake. + +Flags can be forwarded to the formatter by using `--` followed by the flags. + +Any arguments will be forwarded to the formatter. Typically these are the files to format. + +The environment variable `PRJ_ROOT` (according to [prj-spec](https://github.com/numtide/prj-spec)) +will be set to the absolute path to the directory containing the closest parent `flake.nix` +relative to the current directory. + + +# Example + +To use the [official Nix formatter](https://github.com/NixOS/nixfmt): + +```nix +# flake.nix +{ + outputs = { nixpkgs, self }: { + formatter.x86_64-linux = nixpkgs.legacyPackages.${system}.nixfmt-tree; + }; +} +``` + +)"" diff --git a/src/nix/formatter.cc b/src/nix/formatter.cc new file mode 100644 index 00000000000..212bb8d7091 --- /dev/null +++ b/src/nix/formatter.cc @@ -0,0 +1,160 @@ +#include "nix/cmd/command.hh" +#include "nix/cmd/installable-flake.hh" +#include "nix/cmd/installable-value.hh" +#include "nix/expr/eval.hh" +#include "nix/store/local-fs-store.hh" +#include "nix/cmd/installable-derived-path.hh" +#include "nix/util/environment-variables.hh" +#include "run.hh" + +using namespace nix; + +struct CmdFormatter : NixMultiCommand +{ + CmdFormatter() + : NixMultiCommand("formatter", RegisterCommand::getCommandsFor({"formatter"})) + { + } + + std::string description() override + { + return "build or run the formatter"; + } + + Category category() override + { + return catSecondary; + } +}; + +static auto rCmdFormatter = registerCommand("formatter"); + +/** Common implementation bits for the `nix formatter` subcommands. */ +struct MixFormatter : SourceExprCommand +{ + Strings getDefaultFlakeAttrPaths() override + { + return Strings{"formatter." + settings.thisSystem.get()}; + } + + Strings getDefaultFlakeAttrPathPrefixes() override + { + return Strings{}; + } +}; + +struct CmdFormatterRun : MixFormatter, MixJSON +{ + std::vector args; + + CmdFormatterRun() + { + expectArgs({.label = "args", .handler = {&args}}); + } + + std::string description() override + { + return "reformat your code in the standard style"; + } + + std::string doc() override + { + return +#include "formatter-run.md" + ; + } + + Category category() override + { + return catSecondary; + } + + void run(ref store) override + { + auto evalState = getEvalState(); + auto evalStore = getEvalStore(); + + auto installable_ = parseInstallable(store, ".").cast(); + auto & installable = InstallableValue::require(*installable_); + auto app = installable.toApp(*evalState).resolve(evalStore, store); + + auto maybeFlakeDir = installable_->flakeRef.input.getSourcePath(); + assert(maybeFlakeDir.has_value()); + auto flakeDir = maybeFlakeDir.value(); + + Strings programArgs{app.program}; + + // Propagate arguments from the CLI + for (auto & i : args) { + programArgs.push_back(i); + } + + // Add the path to the flake as an environment variable. This enables formatters to format the entire flake even + // if run from a subdirectory. + StringMap env = getEnv(); + env["PRJ_ROOT"] = flakeDir.string(); + + // Release our references to eval caches to ensure they are persisted to disk, because + // we are about to exec out of this process without running C++ destructors. + evalState->evalCaches.clear(); + + execProgramInStore( + store, + UseLookupPath::DontUse, + app.program, + programArgs, + std::nullopt, // Use default system + env); + }; +}; + +static auto rFormatterRun = registerCommand2({"formatter", "run"}); + +struct CmdFormatterBuild : MixFormatter, MixOutLinkByDefault +{ + CmdFormatterBuild() {} + + std::string description() override + { + return "build the current flake's formatter"; + } + + std::string doc() override + { + return +#include "formatter-build.md" + ; + } + + Category category() override + { + return catSecondary; + } + + void run(ref store) override + { + auto evalState = getEvalState(); + auto evalStore = getEvalStore(); + + auto installable_ = parseInstallable(store, "."); + auto & installable = InstallableValue::require(*installable_); + auto unresolvedApp = installable.toApp(*evalState); + auto app = unresolvedApp.resolve(evalStore, store); + auto buildables = unresolvedApp.build(evalStore, store); + createOutLinksMaybe(buildables, store); + + logger->cout("%s", app.program); + }; +}; + +static auto rFormatterBuild = registerCommand2({"formatter", "build"}); + +struct CmdFmt : CmdFormatterRun +{ + void run(ref store) override + { + CmdFormatterRun::run(store); + } +}; + +static auto rFmt = registerCommand("fmt"); diff --git a/src/nix/hash.cc b/src/nix/hash.cc index 2f9b3fe7c6f..510cfa59270 100644 --- a/src/nix/hash.cc +++ b/src/nix/hash.cc @@ -1,13 +1,14 @@ -#include "command.hh" -#include "hash.hh" -#include "content-address.hh" -#include "legacy.hh" -#include "shared.hh" -#include "references.hh" -#include "archive.hh" -#include "git.hh" -#include "posix-source-accessor.hh" -#include "misc-store-flags.hh" +#include "nix/cmd/command.hh" +#include "nix/util/hash.hh" +#include "nix/store/content-address.hh" +#include "nix/cmd/legacy.hh" +#include "nix/main/shared.hh" +#include "nix/util/references.hh" +#include "nix/util/archive.hh" +#include "nix/util/git.hh" +#include "nix/util/posix-source-accessor.hh" +#include "nix/cmd/misc-store-flags.hh" +#include "man-pages.hh" using namespace nix; @@ -87,18 +88,35 @@ struct CmdHashBase : Command return std::make_unique(hashAlgo); }; - auto path2 = PosixSourceAccessor::createAtRoot(path); + auto makeSourcePath = [&]() -> SourcePath { + return PosixSourceAccessor::createAtRoot(makeParentCanonical(path)); + }; + Hash h { HashAlgorithm::SHA256 }; // throwaway def to appease C++ switch (mode) { case FileIngestionMethod::Flat: + { + // While usually we could use the some code as for NixArchive, + // the Flat method needs to support FIFOs, such as those + // produced by bash process substitution, e.g.: + // nix hash --mode flat <(echo hi) + // Also symlinks semantics are unambiguous in the flat case, + // so we don't need to go low-level, or reject symlink `path`s. + auto hashSink = makeSink(); + readFile(path, *hashSink); + h = hashSink->finish().first; + break; + } case FileIngestionMethod::NixArchive: { + auto sourcePath = makeSourcePath(); auto hashSink = makeSink(); - dumpPath(path2, *hashSink, (FileSerialisationMethod) mode); + dumpPath(sourcePath, *hashSink, (FileSerialisationMethod) mode); h = hashSink->finish().first; break; } case FileIngestionMethod::Git: { + auto sourcePath = makeSourcePath(); std::function hook; hook = [&](const SourcePath & path) -> git::TreeEntry { auto hashSink = makeSink(); @@ -109,7 +127,7 @@ struct CmdHashBase : Command .hash = hash, }; }; - h = hook(path2).hash; + h = hook(sourcePath).hash; break; } } @@ -163,8 +181,11 @@ struct CmdToBase : Command HashFormat hashFormat; std::optional hashAlgo; std::vector args; + bool legacyCli; - CmdToBase(HashFormat hashFormat) : hashFormat(hashFormat) + CmdToBase(HashFormat hashFormat, bool legacyCli = false) + : hashFormat(hashFormat) + , legacyCli(legacyCli) { addFlag(flag::hashAlgoOpt("type", &hashAlgo)); expectArgs("strings", &args); @@ -181,7 +202,8 @@ struct CmdToBase : Command void run() override { - warn("The old format conversion sub commands of `nix hash` were deprecated in favor of `nix hash convert`."); + if (!legacyCli) + warn("The old format conversion subcommands of `nix hash` were deprecated in favor of `nix hash convert`."); for (const auto & s : args) logger->cout(Hash::parseAny(s, hashAlgo).to_string(hashFormat, hashFormat == HashFormat::SRI)); } @@ -222,11 +244,18 @@ struct CmdHashConvert : Command Category category() override { return catUtility; } void run() override { - for (const auto& s: hashStrings) { - Hash h = Hash::parseAny(s, algo); - if (from && h.to_string(*from, from == HashFormat::SRI) != s) { + for (const auto & s : hashStrings) { + Hash h = + from == HashFormat::SRI + ? Hash::parseSRI(s) + : Hash::parseAny(s, algo); + if (from + && from != HashFormat::SRI + && h.to_string(*from, false) != + (from == HashFormat::Base16 ? toLower(s) : s)) + { auto from_as_string = printHashFormat(*from); - throw BadHash("input hash '%s' does not have the expected format '--from %s'", s, from_as_string); + throw BadHash("input hash '%s' does not have the expected format for '--from %s'", s, from_as_string); } logger->cout(h.to_string(to, to == HashFormat::SRI)); } @@ -321,7 +350,7 @@ static int compatNixHash(int argc, char * * argv) } else { - CmdToBase cmd(hashFormat); + CmdToBase cmd(hashFormat, true); cmd.args = ss; if (hashAlgo.has_value()) cmd.hashAlgo = hashAlgo; cmd.run(); diff --git a/src/nix/log.cc b/src/nix/log.cc index 1a6f48f5e29..78f1dd570f1 100644 --- a/src/nix/log.cc +++ b/src/nix/log.cc @@ -1,9 +1,8 @@ -#include "command.hh" -#include "common-args.hh" -#include "shared.hh" -#include "store-api.hh" -#include "log-store.hh" -#include "progress-bar.hh" +#include "nix/cmd/command.hh" +#include "nix/main/common-args.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-open.hh" +#include "nix/store/log-store.hh" using namespace nix; @@ -36,7 +35,7 @@ struct CmdLog : InstallableCommand // For compat with CLI today, TODO revisit auto oneUp = std::visit(overloaded { [&](const DerivedPath::Opaque & bo) { - return make_ref(bo); + return make_ref(bo); }, [&](const DerivedPath::Built & bfd) { return bfd.drvPath; @@ -55,7 +54,7 @@ struct CmdLog : InstallableCommand auto log = logSub.getBuildLog(path); if (!log) continue; - stopProgressBar(); + logger->stop(); printInfo("got build log for '%s' from '%s'", installable->what(), logSub.getUri()); writeFull(getStandardOutput(), *log); return; diff --git a/src/nix/ls.cc b/src/nix/ls.cc index 63f97f2d3b6..4b282bc4361 100644 --- a/src/nix/ls.cc +++ b/src/nix/ls.cc @@ -1,15 +1,13 @@ -#include "command.hh" -#include "store-api.hh" -#include "nar-accessor.hh" -#include "common-args.hh" +#include "nix/cmd/command.hh" +#include "nix/store/store-api.hh" +#include "nix/store/nar-accessor.hh" +#include "nix/main/common-args.hh" #include using namespace nix; struct MixLs : virtual Args, MixJSON { - std::string path; - bool recursive = false; bool verbose = false; bool showDirectory = false; @@ -38,7 +36,7 @@ struct MixLs : virtual Args, MixJSON }); } - void listText(ref accessor) + void listText(ref accessor, CanonPath path) { std::function doPath; @@ -77,26 +75,27 @@ struct MixLs : virtual Args, MixJSON showFile(curPath, relPath); }; - auto path2 = CanonPath(path); - auto st = accessor->lstat(path2); - doPath(st, path2, - st.type == SourceAccessor::Type::tDirectory ? "." : path2.baseName().value_or(""), + auto st = accessor->lstat(path); + doPath(st, path, + st.type == SourceAccessor::Type::tDirectory ? "." : path.baseName().value_or(""), showDirectory); } - void list(ref accessor) + void list(ref accessor, CanonPath path) { if (json) { if (showDirectory) throw UsageError("'--directory' is useless with '--json'"); - logger->cout("%s", listNar(accessor, CanonPath(path), recursive)); + logger->cout("%s", listNar(accessor, path, recursive)); } else - listText(accessor); + listText(accessor, std::move(path)); } }; struct CmdLsStore : StoreCommand, MixLs { + std::string path; + CmdLsStore() { expectArgs({ @@ -120,7 +119,8 @@ struct CmdLsStore : StoreCommand, MixLs void run(ref store) override { - list(store->getFSAccessor()); + auto [storePath, rest] = store->toStorePath(path); + list(store->getFSAccessor(), CanonPath{storePath.to_string()} / CanonPath{rest}); } }; @@ -128,6 +128,8 @@ struct CmdLsNar : Command, MixLs { Path narPath; + std::string path; + CmdLsNar() { expectArgs({ @@ -152,7 +154,7 @@ struct CmdLsNar : Command, MixLs void run() override { - list(makeNarAccessor(readFile(narPath))); + list(makeNarAccessor(readFile(narPath)), CanonPath{path}); } }; diff --git a/src/nix/main.cc b/src/nix/main.cc index b0e26e093f1..6144f746ffe 100644 --- a/src/nix/main.cc +++ b/src/nix/main.cc @@ -1,25 +1,30 @@ -#include "args/root.hh" -#include "current-process.hh" -#include "command.hh" -#include "common-args.hh" -#include "eval.hh" -#include "eval-settings.hh" -#include "globals.hh" -#include "legacy.hh" -#include "shared.hh" -#include "store-api.hh" -#include "filetransfer.hh" -#include "finally.hh" -#include "loggers.hh" -#include "markdown.hh" -#include "memory-source-accessor.hh" -#include "terminal.hh" -#include "users.hh" -#include "network-proxy.hh" -#include "eval-cache.hh" -#include "flake/flake.hh" +#include "nix/util/args/root.hh" +#include "nix/util/current-process.hh" +#include "nix/cmd/command.hh" +#include "nix/main/common-args.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/eval-settings.hh" +#include "nix/store/globals.hh" +#include "nix/cmd/legacy.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-open.hh" +#include "nix/store/store-registration.hh" +#include "nix/store/filetransfer.hh" +#include "nix/util/finally.hh" +#include "nix/main/loggers.hh" +#include "nix/cmd/markdown.hh" +#include "nix/util/memory-source-accessor.hh" +#include "nix/util/terminal.hh" +#include "nix/util/users.hh" +#include "nix/cmd/network-proxy.hh" +#include "nix/expr/eval-cache.hh" +#include "nix/flake/flake.hh" +#include "nix/flake/settings.hh" +#include "nix/util/json-utils.hh" + #include "self-exe.hh" -#include "json-utils.hh" +#include "crash-handler.hh" +#include "cli-config-private.hh" #include #include @@ -32,8 +37,8 @@ # include #endif -#if __linux__ -# include "namespaces.hh" +#ifdef __linux__ +# include "nix/util/linux-namespaces.hh" #endif #ifndef _WIN32 @@ -42,23 +47,10 @@ extern std::string chrootHelperName; void chrootHelper(int argc, char * * argv); #endif -#include "strings.hh" +#include "nix/util/strings.hh" namespace nix { -enum struct AliasStatus { - /** Aliases that don't go away */ - AcceptedShorthand, - /** Aliases that will go away */ - Deprecated, -}; - -/** An alias, except for the original syntax, which is in the map key. */ -struct AliasInfo { - AliasStatus status; - std::vector replacement; -}; - /* Check if we have a non-loopback/link-local network interface. */ static bool haveInternet() { @@ -149,54 +141,34 @@ struct NixArgs : virtual MultiCommand, virtual MixCommonArgs, virtual RootArgs .handler = {[&]() { refresh = true; }}, .experimentalFeature = Xp::NixCommand, }); - } - std::map aliases = { - {"add-to-store", { AliasStatus::Deprecated, {"store", "add-path"}}}, - {"cat-nar", { AliasStatus::Deprecated, {"nar", "cat"}}}, - {"cat-store", { AliasStatus::Deprecated, {"store", "cat"}}}, - {"copy-sigs", { AliasStatus::Deprecated, {"store", "copy-sigs"}}}, - {"dev-shell", { AliasStatus::Deprecated, {"develop"}}}, - {"diff-closures", { AliasStatus::Deprecated, {"store", "diff-closures"}}}, - {"dump-path", { AliasStatus::Deprecated, {"store", "dump-path"}}}, - {"hash-file", { AliasStatus::Deprecated, {"hash", "file"}}}, - {"hash-path", { AliasStatus::Deprecated, {"hash", "path"}}}, - {"ls-nar", { AliasStatus::Deprecated, {"nar", "ls"}}}, - {"ls-store", { AliasStatus::Deprecated, {"store", "ls"}}}, - {"make-content-addressable", { AliasStatus::Deprecated, {"store", "make-content-addressed"}}}, - {"optimise-store", { AliasStatus::Deprecated, {"store", "optimise"}}}, - {"ping-store", { AliasStatus::Deprecated, {"store", "info"}}}, - {"sign-paths", { AliasStatus::Deprecated, {"store", "sign"}}}, - {"shell", { AliasStatus::AcceptedShorthand, {"env", "shell"}}}, - {"show-derivation", { AliasStatus::Deprecated, {"derivation", "show"}}}, - {"show-config", { AliasStatus::Deprecated, {"config", "show"}}}, - {"to-base16", { AliasStatus::Deprecated, {"hash", "to-base16"}}}, - {"to-base32", { AliasStatus::Deprecated, {"hash", "to-base32"}}}, - {"to-base64", { AliasStatus::Deprecated, {"hash", "to-base64"}}}, - {"verify", { AliasStatus::Deprecated, {"store", "verify"}}}, - {"doctor", { AliasStatus::Deprecated, {"config", "check"}}}, + aliases = { + {"add-to-store", { AliasStatus::Deprecated, {"store", "add-path"}}}, + {"cat-nar", { AliasStatus::Deprecated, {"nar", "cat"}}}, + {"cat-store", { AliasStatus::Deprecated, {"store", "cat"}}}, + {"copy-sigs", { AliasStatus::Deprecated, {"store", "copy-sigs"}}}, + {"dev-shell", { AliasStatus::Deprecated, {"develop"}}}, + {"diff-closures", { AliasStatus::Deprecated, {"store", "diff-closures"}}}, + {"dump-path", { AliasStatus::Deprecated, {"store", "dump-path"}}}, + {"hash-file", { AliasStatus::Deprecated, {"hash", "file"}}}, + {"hash-path", { AliasStatus::Deprecated, {"hash", "path"}}}, + {"ls-nar", { AliasStatus::Deprecated, {"nar", "ls"}}}, + {"ls-store", { AliasStatus::Deprecated, {"store", "ls"}}}, + {"make-content-addressable", { AliasStatus::Deprecated, {"store", "make-content-addressed"}}}, + {"optimise-store", { AliasStatus::Deprecated, {"store", "optimise"}}}, + {"ping-store", { AliasStatus::Deprecated, {"store", "info"}}}, + {"sign-paths", { AliasStatus::Deprecated, {"store", "sign"}}}, + {"shell", { AliasStatus::AcceptedShorthand, {"env", "shell"}}}, + {"show-derivation", { AliasStatus::Deprecated, {"derivation", "show"}}}, + {"show-config", { AliasStatus::Deprecated, {"config", "show"}}}, + {"to-base16", { AliasStatus::Deprecated, {"hash", "to-base16"}}}, + {"to-base32", { AliasStatus::Deprecated, {"hash", "to-base32"}}}, + {"to-base64", { AliasStatus::Deprecated, {"hash", "to-base64"}}}, + {"verify", { AliasStatus::Deprecated, {"store", "verify"}}}, + {"doctor", { AliasStatus::Deprecated, {"config", "check"}}}, + }; }; - bool aliasUsed = false; - - Strings::iterator rewriteArgs(Strings & args, Strings::iterator pos) override - { - if (aliasUsed || command || pos == args.end()) return pos; - auto arg = *pos; - auto i = aliases.find(arg); - if (i == aliases.end()) return pos; - auto & info = i->second; - if (info.status == AliasStatus::Deprecated) { - warn("'%s' is a deprecated alias for '%s'", - arg, concatStringsSep(" ", info.replacement)); - } - pos = args.erase(pos); - for (auto j = info.replacement.rbegin(); j != info.replacement.rend(); ++j) - pos = args.insert(pos, *j); - aliasUsed = true; - return pos; - } - std::string description() override { return "a tool for reproducible and declarative configuration management"; @@ -222,13 +194,12 @@ struct NixArgs : virtual MultiCommand, virtual MixCommonArgs, virtual RootArgs res["args"] = toJSON(); auto stores = nlohmann::json::object(); - for (auto & implem : *Implementations::registered) { - auto storeConfig = implem.getConfig(); - auto storeName = storeConfig->name(); + for (auto & [storeName, implem] : Implementations::registered()) { auto & j = stores[storeName]; - j["doc"] = storeConfig->doc(); - j["settings"] = storeConfig->toJSON(); - j["experimentalFeature"] = storeConfig->experimentalFeature(); + j["doc"] = implem.doc; + j["uri-schemes"] = implem.uriSchemes; + j["settings"] = implem.getConfig()->toJSON(); + j["experimentalFeature"] = implem.experimentalFeature; } res["stores"] = std::move(stores); res["fetchers"] = fetchers::dumpRegisterInputSchemeInfo(); @@ -354,6 +325,8 @@ void mainWrapped(int argc, char * * argv) { savedArgv = argv; + registerCrashHandler(); + /* The chroot helper needs to be run before any threads have been started. */ #ifndef _WIN32 @@ -365,7 +338,7 @@ void mainWrapped(int argc, char * * argv) initNix(); initGC(); - flake::initLib(flakeSettings); + flakeSettings.configureEvalSettings(evalSettings); /* Set the build hook location @@ -378,7 +351,7 @@ void mainWrapped(int argc, char * * argv) "__build-remote", }); - #if __linux__ + #ifdef __linux__ if (isRootUser()) { try { saveMountNamespace(); @@ -388,8 +361,6 @@ void mainWrapped(int argc, char * * argv) } #endif - Finally f([] { logger->stop(); }); - programPath = argv[0]; auto programName = std::string(baseNameOf(programPath)); auto extensionPos = programName.find_last_of("."); @@ -402,7 +373,7 @@ void mainWrapped(int argc, char * * argv) } { - auto legacy = (*RegisterLegacyCommand::commands)[programName]; + auto legacy = RegisterLegacyCommand::commands()[programName]; if (legacy) return legacy(argc, argv); } @@ -489,6 +460,8 @@ void mainWrapped(int argc, char * * argv) if (!args.helpRequested && !args.completions) throw; } + applyJSONLogger(); + if (args.helpRequested) { std::vector subcommand; MultiCommand * command = &args; @@ -557,9 +530,13 @@ void mainWrapped(int argc, char * * argv) int main(int argc, char * * argv) { + // The CLI has a more detailed version than the libraries; see nixVersion. + nix::nixVersion = NIX_CLI_VERSION; +#ifndef _WIN32 // Increase the default stack size for the evaluator and for // libstdc++'s std::regex. nix::setStackSize(64 * 1024 * 1024); +#endif return nix::handleExceptions(argv[0], [&]() { nix::mainWrapped(argc, argv); diff --git a/src/nix/make-content-addressed.cc b/src/nix/make-content-addressed.cc index d9c988a9f5d..5523ae2790a 100644 --- a/src/nix/make-content-addressed.cc +++ b/src/nix/make-content-addressed.cc @@ -1,7 +1,7 @@ -#include "command.hh" -#include "store-api.hh" -#include "make-content-addressed.hh" -#include "common-args.hh" +#include "nix/cmd/command.hh" +#include "nix/store/store-open.hh" +#include "nix/store/make-content-addressed.hh" +#include "nix/main/common-args.hh" #include @@ -44,7 +44,7 @@ struct CmdMakeContentAddressed : virtual CopyCommand, virtual StorePathsCommand, } auto json = json::object(); json["rewrites"] = jsonRewrites; - logger->cout("%s", json); + printJSON(json); } else { for (auto & path : storePaths) { auto i = remappings.find(path); diff --git a/src/nix/man-pages.cc b/src/nix/man-pages.cc new file mode 100644 index 00000000000..8585c164c44 --- /dev/null +++ b/src/nix/man-pages.cc @@ -0,0 +1,30 @@ +#include "man-pages.hh" +#include "cli-config-private.hh" +#include "nix/util/file-system.hh" +#include "nix/util/current-process.hh" +#include "nix/util/environment-variables.hh" + +namespace nix { + +std::filesystem::path getNixManDir() +{ + return canonPath(NIX_MAN_DIR); +} + +void showManPage(const std::string & name) +{ + restoreProcessContext(); + setEnv("MANPATH", (getNixManDir().string() + ":").c_str()); + execlp("man", "man", name.c_str(), nullptr); + if (errno == ENOENT) { + // Not SysError because we don't want to suffix the errno, aka No such file or directory. + throw Error( + "The '%1%' command was not found, but it is needed for '%2%' and some other '%3%' commands' help text. Perhaps you could install the '%1%' command?", + "man", + name.c_str(), + "nix-*"); + } + throw SysError("command 'man %1%' failed", name.c_str()); +} + +} diff --git a/src/nix/man-pages.hh b/src/nix/man-pages.hh new file mode 100644 index 00000000000..9ba035af816 --- /dev/null +++ b/src/nix/man-pages.hh @@ -0,0 +1,28 @@ +#pragma once +///@file + +#include +#include + +namespace nix { + +/** + * @brief Get path to the nix manual dir. + * + * Nix relies on the man pages being available at a NIX_MAN_DIR for + * displaying help messaged for legacy cli. + * + * NIX_MAN_DIR is a compile-time parameter, so man pages are unlikely to work + * for cases when the nix executable is installed out-of-store or as a static binary. + * + */ +std::filesystem::path getNixManDir(); + +/** + * Show the manual page for the specified program. + * + * @param name Name of the man item. + */ +void showManPage(const std::string & name); + +} diff --git a/src/nix/meson.build b/src/nix/meson.build index 5c70c8216f7..0ba8bdd4640 100644 --- a/src/nix/meson.build +++ b/src/nix/meson.build @@ -4,18 +4,16 @@ project('nix', 'cpp', 'cpp_std=c++2a', # TODO(Qyriad): increase the warning level 'warning_level=1', - 'debug=true', - 'optimization=2', 'errorlogs=true', # Please print logs for tests that fail 'localstatedir=/nix/var', ], - meson_version : '>= 1.1', + meson_version : '>= 1.4', license : 'LGPL-2.1-or-later', ) cxx = meson.get_compiler('cpp') -subdir('build-utils-meson/deps-lists') +subdir('nix-meson-build-support/deps-lists') nix_store = dependency('nix-store') @@ -30,43 +28,36 @@ deps_private_maybe_subproject = [ ] deps_public_maybe_subproject = [ ] -subdir('build-utils-meson/subprojects') +subdir('nix-meson-build-support/subprojects') -subdir('build-utils-meson/export-all-symbols') -subdir('build-utils-meson/windows-version') +subdir('nix-meson-build-support/export-all-symbols') +subdir('nix-meson-build-support/windows-version') configdata = configuration_data() +# The CLI has a more detailed version string than the libraries; see `nixVersion` +configdata.set_quoted('NIX_CLI_VERSION', meson.project_version()) + fs = import('fs') +prefix = get_option('prefix') bindir = get_option('bindir') -if not fs.is_absolute(bindir) - bindir = get_option('prefix') / bindir -endif +bindir = fs.is_absolute(bindir) ? bindir : prefix / bindir configdata.set_quoted('NIX_BIN_DIR', bindir) -config_h = configure_file( - configuration : configdata, - output : 'config-nix-cli.hh', -) +mandir = get_option('mandir') +mandir = fs.is_absolute(mandir) ? mandir : prefix / mandir +configdata.set_quoted('NIX_MAN_DIR', mandir) -add_project_arguments( - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. - '-include', 'config-util.hh', - '-include', 'config-store.hh', - '-include', 'config-expr.hh', - #'-include', 'config-fetchers.hh', - '-include', 'config-main.hh', - '-include', 'config-cmd.hh', - '-include', 'config-nix-cli.hh', - language : 'cpp', +config_priv_h = configure_file( + configuration : configdata, + output : 'cli-config-private.hh', ) -subdir('build-utils-meson/common') -subdir('build-utils-meson/generate-header') +subdir('nix-meson-build-support/common') +subdir('nix-meson-build-support/generate-header') -nix_sources = [config_h] + files( +nix_sources = [config_priv_h] + files( 'add-to-store.cc', 'app.cc', 'self-exe.cc', @@ -76,6 +67,7 @@ nix_sources = [config_h] + files( 'config-check.cc', 'config.cc', 'copy.cc', + 'crash-handler.cc', 'derivation-add.cc', 'derivation-show.cc', 'derivation.cc', @@ -86,12 +78,13 @@ nix_sources = [config_h] + files( 'env.cc', 'eval.cc', 'flake.cc', - 'fmt.cc', + 'formatter.cc', 'hash.cc', 'log.cc', 'ls.cc', 'main.cc', 'make-content-addressed.cc', + 'man-pages.cc', 'nar.cc', 'optimise-store.cc', 'path-from-hash-part.cc', @@ -225,7 +218,7 @@ foreach linkname : nix_symlinks # The 'runtime' tag is what executables default to, which we want to emulate here. install_tag : 'runtime' ) - t = custom_target( + custom_target( command: ['ln', '-sf', fs.name(this_exe), '@OUTPUT@'], output: linkname + executable_suffix, # native doesn't allow dangling symlinks, but the target executable often doesn't exist at this time @@ -233,7 +226,7 @@ foreach linkname : nix_symlinks # TODO(Ericson2314): Don't do this once we have the `meson.override_find_program` working) build_by_default: true ) - # TODO(Ericson3214): Dosen't yet work + # TODO(Ericson3214): Doesn't yet work #meson.override_find_program(linkname, t) endforeach @@ -253,7 +246,7 @@ custom_target( # TODO(Ericson2314): Don't do this once we have the `meson.override_find_program` working) build_by_default: true ) -# TODO(Ericson3214): Dosen't yet work +# TODO(Ericson3214): Doesn't yet work #meson.override_find_program(linkname, t) localstatedir = nix_store.get_variable( diff --git a/src/nix/nar.cc b/src/nix/nar.cc index 8ad4f92a796..debb6b95e4e 100644 --- a/src/nix/nar.cc +++ b/src/nix/nar.cc @@ -1,4 +1,4 @@ -#include "command.hh" +#include "nix/cmd/command.hh" using namespace nix; diff --git a/src/nix/nix-meson-build-support b/src/nix/nix-meson-build-support new file mode 120000 index 00000000000..0b140f56bde --- /dev/null +++ b/src/nix/nix-meson-build-support @@ -0,0 +1 @@ +../../nix-meson-build-support \ No newline at end of file diff --git a/src/nix/optimise-store.cc b/src/nix/optimise-store.cc index 985006e5a54..e319f5c9081 100644 --- a/src/nix/optimise-store.cc +++ b/src/nix/optimise-store.cc @@ -1,6 +1,6 @@ -#include "command.hh" -#include "shared.hh" -#include "store-api.hh" +#include "nix/cmd/command.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-api.hh" #include diff --git a/src/nix/package.nix b/src/nix/package.nix index c7b24efce72..3d4f6f40b4f 100644 --- a/src/nix/package.nix +++ b/src/nix/package.nix @@ -1,15 +1,16 @@ -{ lib -, stdenv -, mkMesonExecutable +{ + stdenv, + lib, + mkMesonExecutable, -, nix-store -, nix-expr -, nix-main -, nix-cmd + nix-store, + nix-expr, + nix-main, + nix-cmd, -# Configuration Options + # Configuration Options -, version + version, }: let @@ -21,64 +22,67 @@ mkMesonExecutable (finalAttrs: { inherit version; workDir = ./.; - fileset = fileset.unions ([ - ../../build-utils-meson - ./build-utils-meson - ../../.version - ./.version - ./meson.build - ./meson.options - - # Symbolic links to other dirs - ## exes - ./build-remote - ./doc - ./nix-build - ./nix-channel - ./nix-collect-garbage - ./nix-copy-closure - ./nix-env - ./nix-instantiate - ./nix-store - ## dirs - ./scripts - ../../scripts - ./misc - ../../misc - - # Doc nix files for --help - ../../doc/manual/generate-manpage.nix - ../../doc/manual/utils.nix - ../../doc/manual/generate-settings.nix - ../../doc/manual/generate-store-info.nix - - # Other files to be included as string literals - ../nix-channel/unpack-channel.nix - ../nix-env/buildenv.nix - ./get-env.sh - ./help-stores.md - ../../doc/manual/source/store/types/index.md.in - ./profiles.md - ../../doc/manual/source/command-ref/files/profiles.md - - # Files - ] ++ lib.concatMap - (dir: [ - (fileset.fileFilter (file: file.hasExt "cc") dir) - (fileset.fileFilter (file: file.hasExt "hh") dir) - (fileset.fileFilter (file: file.hasExt "md") dir) - ]) + fileset = fileset.unions ( [ - ./. - ../build-remote - ../nix-build - ../nix-channel - ../nix-collect-garbage - ../nix-copy-closure - ../nix-env - ../nix-instantiate - ../nix-store + ../../nix-meson-build-support + ./nix-meson-build-support + ../../.version + ./.version + ./meson.build + ./meson.options + + # Symbolic links to other dirs + ## exes + ./build-remote + ./doc + ./nix-build + ./nix-channel + ./nix-collect-garbage + ./nix-copy-closure + ./nix-env + ./nix-instantiate + ./nix-store + ## dirs + ./scripts + ../../scripts + ./misc + ../../misc + + # Doc nix files for --help + ../../doc/manual/generate-manpage.nix + ../../doc/manual/utils.nix + ../../doc/manual/generate-settings.nix + ../../doc/manual/generate-store-info.nix + + # Other files to be included as string literals + ../nix-channel/unpack-channel.nix + ../nix-env/buildenv.nix + ./get-env.sh + ./help-stores.md + ../../doc/manual/source/store/types/index.md.in + ./profiles.md + ../../doc/manual/source/command-ref/files/profiles.md + + # Files ] + ++ + lib.concatMap + (dir: [ + (fileset.fileFilter (file: file.hasExt "cc") dir) + (fileset.fileFilter (file: file.hasExt "hh") dir) + (fileset.fileFilter (file: file.hasExt "md") dir) + ]) + [ + ./. + ../build-remote + ../nix-build + ../nix-channel + ../nix-collect-garbage + ../nix-copy-closure + ../nix-env + ../nix-instantiate + ../nix-store + ] ); buildInputs = [ @@ -88,22 +92,16 @@ mkMesonExecutable (finalAttrs: { nix-cmd ]; - preConfigure = - # "Inline" .version so it's not a symlink, and includes the suffix. - # Do the meson utils, without modification. - '' - chmod u+w ./.version - echo ${version} > ../../../.version - ''; - mesonFlags = [ ]; - env = lib.optionalAttrs (stdenv.isLinux && !(stdenv.hostPlatform.isStatic && stdenv.system == "aarch64-linux")) { - LDFLAGS = "-fuse-ld=gold"; - }; + postInstall = lib.optionalString stdenv.hostPlatform.isStatic '' + mkdir -p $out/nix-support + echo "file binary-dist $out/bin/nix" >> $out/nix-support/hydra-build-products + ''; meta = { + mainProgram = "nix"; platforms = lib.platforms.unix ++ lib.platforms.windows; }; diff --git a/src/nix/path-from-hash-part.cc b/src/nix/path-from-hash-part.cc index 7f7cda8d3d3..814b723f9b0 100644 --- a/src/nix/path-from-hash-part.cc +++ b/src/nix/path-from-hash-part.cc @@ -1,5 +1,5 @@ -#include "command.hh" -#include "store-api.hh" +#include "nix/cmd/command.hh" +#include "nix/store/store-api.hh" using namespace nix; diff --git a/src/nix/path-info.cc b/src/nix/path-info.cc index 8e3d0406dd2..04af72646e7 100644 --- a/src/nix/path-info.cc +++ b/src/nix/path-info.cc @@ -1,15 +1,15 @@ -#include "command.hh" -#include "shared.hh" -#include "store-api.hh" -#include "common-args.hh" -#include "nar-info.hh" +#include "nix/cmd/command.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-api.hh" +#include "nix/main/common-args.hh" +#include "nix/store/nar-info.hh" #include #include #include -#include "strings.hh" +#include "nix/util/strings.hh" using namespace nix; using nlohmann::json; @@ -154,11 +154,11 @@ struct CmdPathInfo : StorePathsCommand, MixJSON pathLen = std::max(pathLen, store->printStorePath(storePath).size()); if (json) { - logger->cout(pathInfoToJSON( + printJSON(pathInfoToJSON( *store, // FIXME: preserve order? StorePathSet(storePaths.begin(), storePaths.end()), - showClosureSize).dump()); + showClosureSize)); } else { diff --git a/src/nix/prefetch.cc b/src/nix/prefetch.cc index db7d9e4efe6..96dcdb4e87a 100644 --- a/src/nix/prefetch.cc +++ b/src/nix/prefetch.cc @@ -1,17 +1,19 @@ -#include "command.hh" -#include "common-args.hh" -#include "shared.hh" -#include "store-api.hh" -#include "filetransfer.hh" -#include "finally.hh" -#include "progress-bar.hh" -#include "tarfile.hh" -#include "attr-path.hh" -#include "eval-inline.hh" -#include "legacy.hh" -#include "posix-source-accessor.hh" -#include "misc-store-flags.hh" -#include "terminal.hh" +#include "nix/cmd/command.hh" +#include "nix/main/common-args.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-open.hh" +#include "nix/store/filetransfer.hh" +#include "nix/util/finally.hh" +#include "nix/main/loggers.hh" +#include "nix/util/tarfile.hh" +#include "nix/expr/attr-path.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/cmd/legacy.hh" +#include "nix/util/posix-source-accessor.hh" +#include "nix/cmd/misc-store-flags.hh" +#include "nix/util/terminal.hh" + +#include "man-pages.hh" #include @@ -44,7 +46,7 @@ std::string resolveMirrorUrl(EvalState & state, const std::string & url) if (mirrorList->value->listSize() < 1) throw Error("mirror URL '%s' did not expand to anything", url); - std::string mirror(state.forceString(*mirrorList->value->listElems()[0], noPos, "while evaluating the first available mirror")); + std::string mirror(state.forceString(*mirrorList->value->listView()[0], noPos, "while evaluating the first available mirror")); return mirror + (hasSuffix(mirror, "/") ? "" : "/") + s.substr(p + 1); } @@ -114,11 +116,11 @@ std::tuple prefetchFile( createDirs(unpacked); unpackTarfile(tmpFile.string(), unpacked); - auto entries = std::filesystem::directory_iterator{unpacked}; + auto entries = DirectoryIterator{unpacked}; /* If the archive unpacks to a single file/directory, then use that as the top-level. */ tmpFile = entries->path(); - auto fileCount = std::distance(entries, std::filesystem::directory_iterator{}); + auto fileCount = std::distance(entries, DirectoryIterator{}); if (fileCount != 1) { /* otherwise, use the directory itself */ tmpFile = unpacked; @@ -189,10 +191,7 @@ static int main_nix_prefetch_url(int argc, char * * argv) if (args.size() > 2) throw UsageError("too many arguments"); - Finally f([]() { stopProgressBar(); }); - - if (isTTY()) - startProgressBar(); + setLogFormat("bar"); auto store = openStore(); auto state = std::make_unique(myArgs.lookupPath, store, fetchSettings, evalSettings); @@ -222,7 +221,7 @@ static int main_nix_prefetch_url(int argc, char * * argv) state->forceList(*attr->value, noPos, "while evaluating the urls to prefetch"); if (attr->value->listSize() < 1) throw Error("'urls' list is empty"); - url = state->forceString(*attr->value->listElems()[0], noPos, "while evaluating the first url from the urls list"); + url = state->forceString(*attr->value->listView()[0], noPos, "while evaluating the first url from the urls list"); /* Extract the hash mode. */ auto attr2 = v.attrs()->get(state->symbols.create("outputHashMode")); @@ -246,7 +245,7 @@ static int main_nix_prefetch_url(int argc, char * * argv) auto [storePath, hash] = prefetchFile( store, resolveMirrorUrl(*state, url), name, ha, expectedHash, unpack, executable); - stopProgressBar(); + logger->stop(); if (!printPath) printInfo("path is '%s'", store->printStorePath(storePath)); @@ -276,7 +275,7 @@ struct CmdStorePrefetchFile : StoreCommand, MixJSON .longName = "name", .description = "Override the name component of the resulting store path. It defaults to the base name of *url*.", .labels = {"name"}, - .handler = {&name} + .handler = {&name}, }); addFlag({ @@ -285,7 +284,7 @@ struct CmdStorePrefetchFile : StoreCommand, MixJSON .labels = {"hash"}, .handler = {[&](std::string s) { expectedHash = Hash::parseAny(s, hashAlgo); - }} + }}, }); addFlag(flag::hashAlgo("hash-type", &hashAlgo)); @@ -328,7 +327,7 @@ struct CmdStorePrefetchFile : StoreCommand, MixJSON auto res = nlohmann::json::object(); res["storePath"] = store->printStorePath(storePath); res["hash"] = hash.to_string(HashFormat::SRI, true); - logger->cout(res.dump()); + printJSON(res); } else { notice("Downloaded '%s' to '%s' (hash '%s').", url, diff --git a/src/nix/profile-add.md b/src/nix/profile-add.md new file mode 100644 index 00000000000..f1d5391efa7 --- /dev/null +++ b/src/nix/profile-add.md @@ -0,0 +1,37 @@ +R""( + +# Examples + +- Add a package from Nixpkgs: + + ```console + # nix profile add nixpkgs#hello + ``` + +- Add a package from a specific branch of Nixpkgs: + + ```console + # nix profile add nixpkgs/release-20.09#hello + ``` + +- Add a package from a specific revision of Nixpkgs: + + ```console + # nix profile add nixpkgs/d73407e8e6002646acfdef0e39ace088bacc83da#hello + ``` + +- Add a specific output of a package: + + ```console + # nix profile add nixpkgs#bash^man + ``` + +# Description + +This command adds [_installables_](./nix.md#installables) to a Nix profile. + +> **Note** +> +> `nix profile install` is an alias for `nix profile add`. + +)"" diff --git a/src/nix/profile-install.md b/src/nix/profile-install.md deleted file mode 100644 index 4c0f82c09e5..00000000000 --- a/src/nix/profile-install.md +++ /dev/null @@ -1,34 +0,0 @@ -R""( - -# Examples - -* Install a package from Nixpkgs: - - ```console - # nix profile install nixpkgs#hello - ``` - -* Install a package from a specific branch of Nixpkgs: - - ```console - # nix profile install nixpkgs/release-20.09#hello - ``` - -* Install a package from a specific revision of Nixpkgs: - - ```console - # nix profile install nixpkgs/d73407e8e6002646acfdef0e39ace088bacc83da#hello - ``` - -* Install a specific output of a package: - - ```console - # nix profile install nixpkgs#bash^man - ``` - - -# Description - -This command adds [*installables*](./nix.md#installables) to a Nix profile. - -)"" diff --git a/src/nix/profile.cc b/src/nix/profile.cc index 324fd633003..2c593729f49 100644 --- a/src/nix/profile.cc +++ b/src/nix/profile.cc @@ -1,23 +1,23 @@ -#include "command.hh" -#include "installable-flake.hh" -#include "common-args.hh" -#include "shared.hh" -#include "store-api.hh" -#include "derivations.hh" -#include "archive.hh" -#include "builtins/buildenv.hh" -#include "flake/flakeref.hh" +#include "nix/cmd/command.hh" +#include "nix/cmd/installable-flake.hh" +#include "nix/main/common-args.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-api.hh" +#include "nix/store/derivations.hh" +#include "nix/util/archive.hh" +#include "nix/store/builtins/buildenv.hh" +#include "nix/flake/flakeref.hh" #include "../nix-env/user-env.hh" -#include "profiles.hh" -#include "names.hh" -#include "url.hh" -#include "flake/url-name.hh" +#include "nix/store/profiles.hh" +#include "nix/store/names.hh" +#include "nix/util/url.hh" +#include "nix/flake/url-name.hh" #include #include #include -#include "strings.hh" +#include "nix/util/strings.hh" using namespace nix; @@ -67,7 +67,7 @@ struct ProfileElement * Return a string representing an installable corresponding to the current * element, either a flakeref or a plain store path */ - std::set toInstallables(Store & store) + StringSet toInstallables(Store & store) { if (source) return {source->to_string()}; @@ -338,14 +338,14 @@ builtPathsPerInstallable( return res; } -struct CmdProfileInstall : InstallablesCommand, MixDefaultProfile +struct CmdProfileAdd : InstallablesCommand, MixDefaultProfile { std::optional priority; - CmdProfileInstall() { + CmdProfileAdd() { addFlag({ .longName = "priority", - .description = "The priority of the package to install.", + .description = "The priority of the package to add.", .labels = {"priority"}, .handler = {&priority}, }); @@ -353,13 +353,13 @@ struct CmdProfileInstall : InstallablesCommand, MixDefaultProfile std::string description() override { - return "install a package into a profile"; + return "add a package to a profile"; } std::string doc() override { return - #include "profile-install.md" + #include "profile-add.md" ; } @@ -415,7 +415,7 @@ struct CmdProfileInstall : InstallablesCommand, MixDefaultProfile && existingSource->originalRef == elementSource->originalRef && existingSource->attrPath == elementSource->attrPath ) { - warn("'%s' is already installed", elementName); + warn("'%s' is already added", elementName); continue; } } @@ -462,15 +462,15 @@ struct CmdProfileInstall : InstallablesCommand, MixDefaultProfile "\n" " nix profile remove %3%\n" "\n" - "The new package can also be installed next to the existing one by assigning a different priority.\n" + "The new package can also be added next to the existing one by assigning a different priority.\n" "The conflicting packages have a priority of %5%.\n" "To prioritise the new package:\n" "\n" - " nix profile install %4% --priority %6%\n" + " nix profile add %4% --priority %6%\n" "\n" "To prioritise the existing package:\n" "\n" - " nix profile install %4% --priority %7%\n", + " nix profile add %4% --priority %7%\n", originalConflictingFilePath, newConflictingFilePath, originalEntryName, @@ -600,7 +600,7 @@ class MixProfileElementMatchers : virtual Args, virtual StoreCommand }); } - std::set getMatchingElementNames(ProfileManifest & manifest) { + StringSet getMatchingElementNames(ProfileManifest & manifest) { if (_matchers.empty()) { throw UsageError("No packages specified."); } @@ -614,7 +614,7 @@ class MixProfileElementMatchers : virtual Args, virtual StoreCommand return {}; } - std::set result; + StringSet result; for (auto & matcher : _matchers) { bool foundMatch = false; for (auto & [name, element] : manifest.elements) { @@ -708,16 +708,14 @@ struct CmdProfileUpgrade : virtual SourceExprCommand, MixDefaultProfile, MixProf if (!element.source) { warn( - "Found package '%s', but it was not installed from a flake, so it can't be checked for upgrades!", - element.identifier() - ); + "Found package '%s', but it was not added from a flake, so it can't be checked for upgrades!", + element.identifier()); continue; } if (element.source->originalRef.input.isLocked()) { warn( - "Found package '%s', but it was installed from a locked flake reference so it can't be upgraded!", - element.identifier() - ); + "Found package '%s', but it was added from a locked flake reference so it can't be upgraded!", + element.identifier()); continue; } @@ -787,7 +785,7 @@ struct CmdProfileList : virtual EvalCommand, virtual StoreCommand, MixDefaultPro { std::string description() override { - return "list installed packages"; + return "list packages in the profile"; } std::string doc() override @@ -802,7 +800,7 @@ struct CmdProfileList : virtual EvalCommand, virtual StoreCommand, MixDefaultPro ProfileManifest manifest(*getEvalState(), *profile); if (json) { - std::cout << manifest.toJSON(*store).dump() << "\n"; + printJSON(manifest.toJSON(*store)); } else { for (const auto & [i, e] : enumerate(manifest.elements)) { auto & [name, element] = e; @@ -978,7 +976,7 @@ struct CmdProfile : NixMultiCommand : NixMultiCommand( "profile", { - {"install", []() { return make_ref(); }}, + {"add", []() { return make_ref(); }}, {"remove", []() { return make_ref(); }}, {"upgrade", []() { return make_ref(); }}, {"list", []() { return make_ref(); }}, @@ -987,7 +985,11 @@ struct CmdProfile : NixMultiCommand {"rollback", []() { return make_ref(); }}, {"wipe-history", []() { return make_ref(); }}, }) - { } + { + aliases = { + {"install", { AliasStatus::Deprecated, {"add"}}}, + }; + } std::string description() override { diff --git a/src/nix/realisation.cc b/src/nix/realisation.cc index a386d98eac9..f21567639ec 100644 --- a/src/nix/realisation.cc +++ b/src/nix/realisation.cc @@ -1,5 +1,5 @@ -#include "command.hh" -#include "common-args.hh" +#include "nix/cmd/command.hh" +#include "nix/main/common-args.hh" #include @@ -57,7 +57,7 @@ struct CmdRealisationInfo : BuiltPathsCommand, MixJSON res.push_back(currentPath); } - logger->cout("%s", res); + printJSON(res); } else { for (auto & path : realisations) { diff --git a/src/nix/registry.cc b/src/nix/registry.cc index ee45162302c..340d10ec42e 100644 --- a/src/nix/registry.cc +++ b/src/nix/registry.cc @@ -1,11 +1,11 @@ -#include "command.hh" -#include "common-args.hh" -#include "shared.hh" -#include "eval.hh" -#include "flake/flake.hh" -#include "store-api.hh" -#include "fetchers.hh" -#include "registry.hh" +#include "nix/cmd/command.hh" +#include "nix/main/common-args.hh" +#include "nix/main/shared.hh" +#include "nix/expr/eval.hh" +#include "nix/flake/flake.hh" +#include "nix/store/store-api.hh" +#include "nix/fetchers/fetchers.hh" +#include "nix/fetchers/registry.hh" using namespace nix; using namespace nix::flake; diff --git a/src/nix/registry.md b/src/nix/registry.md index bd3575d1b5a..d6f8af5e958 100644 --- a/src/nix/registry.md +++ b/src/nix/registry.md @@ -34,6 +34,8 @@ highest precedence: * Overrides specified on the command line using the option `--override-flake`. +Note that the system and user registries are not used to resolve flake references in `flake.nix`. They are only used to resolve flake references on the command line. + # Registry format A registry is a JSON file with the following format: diff --git a/src/nix/repl.cc b/src/nix/repl.cc index 5a570749f4c..ca470e99bce 100644 --- a/src/nix/repl.cc +++ b/src/nix/repl.cc @@ -1,11 +1,12 @@ -#include "eval.hh" -#include "eval-settings.hh" -#include "config-global.hh" -#include "globals.hh" -#include "command.hh" -#include "installable-value.hh" -#include "repl.hh" -#include "processes.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/eval-settings.hh" +#include "nix/util/config-global.hh" +#include "nix/store/globals.hh" +#include "nix/store/store-open.hh" +#include "nix/cmd/command.hh" +#include "nix/cmd/installable-value.hh" +#include "nix/cmd/repl.hh" +#include "nix/util/processes.hh" #include "self-exe.hh" namespace nix { diff --git a/src/nix/run.cc b/src/nix/run.cc index a9f9ef60ff2..3dae8ebc97d 100644 --- a/src/nix/run.cc +++ b/src/nix/run.cc @@ -1,25 +1,27 @@ -#include "current-process.hh" +#include "nix/util/current-process.hh" #include "run.hh" -#include "command-installable-value.hh" -#include "common-args.hh" -#include "shared.hh" -#include "signals.hh" -#include "store-api.hh" -#include "derivations.hh" -#include "local-fs-store.hh" -#include "finally.hh" -#include "source-accessor.hh" -#include "progress-bar.hh" -#include "eval.hh" +#include "nix/cmd/command-installable-value.hh" +#include "nix/main/common-args.hh" +#include "nix/main/shared.hh" +#include "nix/util/signals.hh" +#include "nix/store/store-api.hh" +#include "nix/store/derivations.hh" +#include "nix/store/local-fs-store.hh" +#include "nix/util/finally.hh" +#include "nix/util/source-accessor.hh" +#include "nix/expr/eval.hh" +#include "nix/util/util.hh" #include -#if __linux__ +#ifdef __linux__ # include -# include "personality.hh" +# include "nix/store/personality.hh" #endif #include +extern char ** environ __attribute__((weak)); + namespace nix::fs { using namespace std::filesystem; } using namespace nix; @@ -28,13 +30,36 @@ std::string chrootHelperName = "__run_in_chroot"; namespace nix { +/* Convert `env` to a list of strings suitable for `execve`'s `envp` argument. */ +Strings toEnvp(StringMap env) +{ + Strings envStrs; + for (auto & i : env) { + envStrs.push_back(i.first + "=" + i.second); + } + + return envStrs; +} + void execProgramInStore(ref store, UseLookupPath useLookupPath, const std::string & program, const Strings & args, - std::optional system) + std::optional system, + std::optional env) { - stopProgressBar(); + logger->stop(); + + char **envp; + Strings envStrs; + std::vector envCharPtrs; + if (env.has_value()) { + envStrs = toEnvp(env.value()); + envCharPtrs = stringsToCharPtrs(envStrs); + envp = envCharPtrs.data(); + } else { + envp = environ; + } restoreProcessContext(); @@ -55,20 +80,22 @@ void execProgramInStore(ref store, Strings helperArgs = { chrootHelperName, store->storeDir, store2->getRealStoreDir(), std::string(system.value_or("")), program }; for (auto & arg : args) helperArgs.push_back(arg); - execv(getSelfExe().value_or("nix").c_str(), stringsToCharPtrs(helperArgs).data()); + execve(getSelfExe().value_or("nix").c_str(), stringsToCharPtrs(helperArgs).data(), envp); throw SysError("could not execute chroot helper"); } -#if __linux__ +#ifdef __linux__ if (system) linux::setPersonality(*system); #endif - if (useLookupPath == UseLookupPath::Use) + if (useLookupPath == UseLookupPath::Use) { + // We have to set `environ` by hand because there is no `execvpe` on macOS. + environ = envp; execvp(program.c_str(), stringsToCharPtrs(args).data()); - else - execv(program.c_str(), stringsToCharPtrs(args).data()); + } else + execve(program.c_str(), stringsToCharPtrs(args).data(), envp); throw SysError("unable to execute '%s'", program); } @@ -154,7 +181,7 @@ void chrootHelper(int argc, char * * argv) while (p < argc) args.push_back(argv[p++]); -#if __linux__ +#ifdef __linux__ uid_t uid = getuid(); uid_t gid = getgid(); @@ -173,25 +200,25 @@ void chrootHelper(int argc, char * * argv) if (!pathExists(storeDir)) { // FIXME: Use overlayfs? - fs::path tmpDir = createTempDir(); + std::filesystem::path tmpDir = createTempDir(); createDirs(tmpDir + storeDir); if (mount(realStoreDir.c_str(), (tmpDir + storeDir).c_str(), "", MS_BIND, 0) == -1) throw SysError("mounting '%s' on '%s'", realStoreDir, storeDir); - for (const auto & entry : fs::directory_iterator{"/"}) { + for (const auto & entry : DirectoryIterator{"/"}) { checkInterrupt(); const auto & src = entry.path(); - fs::path dst = tmpDir / entry.path().filename(); + std::filesystem::path dst = tmpDir / entry.path().filename(); if (pathExists(dst)) continue; auto st = entry.symlink_status(); - if (fs::is_directory(st)) { + if (std::filesystem::is_directory(st)) { if (mkdir(dst.c_str(), 0700) == -1) throw SysError("creating directory '%s'", dst); if (mount(src.c_str(), dst.c_str(), "", MS_BIND | MS_REC, 0) == -1) throw SysError("mounting '%s' on '%s'", src, dst); - } else if (fs::is_symlink(st)) + } else if (std::filesystem::is_symlink(st)) createSymlink(readLink(src), dst); } @@ -209,11 +236,11 @@ void chrootHelper(int argc, char * * argv) if (mount(realStoreDir.c_str(), storeDir.c_str(), "", MS_BIND, 0) == -1) throw SysError("mounting '%s' on '%s'", realStoreDir, storeDir); - writeFile(fs::path{"/proc/self/setgroups"}, "deny"); - writeFile(fs::path{"/proc/self/uid_map"}, fmt("%d %d %d", uid, uid, 1)); - writeFile(fs::path{"/proc/self/gid_map"}, fmt("%d %d %d", gid, gid, 1)); + writeFile(std::filesystem::path{"/proc/self/setgroups"}, "deny"); + writeFile(std::filesystem::path{"/proc/self/uid_map"}, fmt("%d %d %d", uid, uid, 1)); + writeFile(std::filesystem::path{"/proc/self/gid_map"}, fmt("%d %d %d", gid, gid, 1)); -#if __linux__ +#ifdef __linux__ if (system != "") linux::setPersonality(system); #endif diff --git a/src/nix/run.hh b/src/nix/run.hh index 51517fdc94a..5367c515c1f 100644 --- a/src/nix/run.hh +++ b/src/nix/run.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "store-api.hh" +#include "nix/store/store-api.hh" namespace nix { @@ -14,6 +14,7 @@ void execProgramInStore(ref store, UseLookupPath useLookupPath, const std::string & program, const Strings & args, - std::optional system = std::nullopt); + std::optional system = std::nullopt, + std::optional env = std::nullopt); } diff --git a/src/nix/search.cc b/src/nix/search.cc index 30b96c5008d..306a8059421 100644 --- a/src/nix/search.cc +++ b/src/nix/search.cc @@ -1,22 +1,22 @@ -#include "command-installable-value.hh" -#include "globals.hh" -#include "eval.hh" -#include "eval-inline.hh" -#include "eval-settings.hh" -#include "names.hh" -#include "get-drvs.hh" -#include "common-args.hh" -#include "shared.hh" -#include "eval-cache.hh" -#include "attr-path.hh" -#include "hilite.hh" -#include "strings-inline.hh" +#include "nix/cmd/command-installable-value.hh" +#include "nix/store/globals.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/eval-inline.hh" +#include "nix/expr/eval-settings.hh" +#include "nix/store/names.hh" +#include "nix/expr/get-drvs.hh" +#include "nix/main/common-args.hh" +#include "nix/main/shared.hh" +#include "nix/expr/eval-cache.hh" +#include "nix/expr/attr-path.hh" +#include "nix/util/hilite.hh" +#include "nix/util/strings-inline.hh" #include #include #include -#include "strings.hh" +#include "nix/util/strings.hh" using namespace nix; using json = nlohmann::json; @@ -198,7 +198,7 @@ struct CmdSearch : InstallableValueCommand, MixJSON visit(*cursor, cursor->getAttrPath(), true); if (json) - logger->cout("%s", *jsonOut); + printJSON(*jsonOut); if (!json && !results) throw Error("no results for the given search term(s)!"); diff --git a/src/nix/search.md b/src/nix/search.md index f65ac9b1748..d355a7764dc 100644 --- a/src/nix/search.md +++ b/src/nix/search.md @@ -62,8 +62,8 @@ R""( # Description -`nix search` searches [*installable*](./nix.md#installables) (which can be evaluated, that is, a -flake or Nix expression, but not a store path or store derivation path) for packages whose name or description matches all of the +`nix search` searches [*installable*](./nix.md#installables) that can be evaluated, that is, a +flake or Nix expression, but not a [store path] or [deriving path]) for packages whose name or description matches all of the regular expressions *regex*. For each matching package, It prints the full attribute name (from the root of the [installable](./nix.md#installables)), the version and the `meta.description` field, highlighting the substrings that @@ -75,6 +75,9 @@ it avoids highlighting the entire name and description of every package. > Note that in this context, `^` is the regex character to match the beginning of a string, *not* the delimiter for > [selecting a derivation output](@docroot@/command-ref/new-cli/nix.md#derivation-output-selection). +[store path]: @docroot@/glossary.md#gloss-store-path +[deriving path]: @docroot@/glossary.md#gloss-deriving-path + # Flake output attributes If no flake output attribute is given, `nix search` searches for diff --git a/src/nix/self-exe.cc b/src/nix/self-exe.cc index 77d20a835e3..b5eb1190d07 100644 --- a/src/nix/self-exe.cc +++ b/src/nix/self-exe.cc @@ -1,36 +1,34 @@ -#include "current-process.hh" -#include "file-system.hh" -#include "globals.hh" +#include "nix/util/current-process.hh" +#include "nix/util/file-system.hh" +#include "nix/store/globals.hh" + #include "self-exe.hh" +#include "cli-config-private.hh" namespace nix { -namespace fs { -using namespace std::filesystem; -} - -fs::path getNixBin(std::optional binaryNameOpt) +std::filesystem::path getNixBin(std::optional binaryNameOpt) { auto getBinaryName = [&] { return binaryNameOpt ? *binaryNameOpt : "nix"; }; // If the environment variable is set, use it unconditionally. if (auto envOpt = getEnvNonEmpty("NIX_BIN_DIR")) - return fs::path{*envOpt} / std::string{getBinaryName()}; + return std::filesystem::path{*envOpt} / std::string{getBinaryName()}; // Try OS tricks, if available, to get to the path of this Nix, and // see if we can find the right executable next to that. if (auto selfOpt = getSelfExe()) { - fs::path path{*selfOpt}; + std::filesystem::path path{*selfOpt}; if (binaryNameOpt) path = path.parent_path() / std::string{*binaryNameOpt}; - if (fs::exists(path)) + if (std::filesystem::exists(path)) return path; } // If `nix` exists at the hardcoded fallback path, use it. { - auto path = fs::path{NIX_BIN_DIR} / std::string{getBinaryName()}; - if (fs::exists(path)) + auto path = std::filesystem::path{NIX_BIN_DIR} / std::string{getBinaryName()}; + if (std::filesystem::exists(path)) return path; } diff --git a/src/nix/self-exe.hh b/src/nix/self-exe.hh index 3161553ecfa..91e260f0b79 100644 --- a/src/nix/self-exe.hh +++ b/src/nix/self-exe.hh @@ -2,6 +2,8 @@ ///@file #include +#include +#include namespace nix { diff --git a/src/nix/sigs.cc b/src/nix/sigs.cc index 134d4f34a17..fb868baa1f2 100644 --- a/src/nix/sigs.cc +++ b/src/nix/sigs.cc @@ -1,9 +1,8 @@ -#include "signals.hh" -#include "command.hh" -#include "shared.hh" -#include "store-api.hh" -#include "thread-pool.hh" -#include "progress-bar.hh" +#include "nix/util/signals.hh" +#include "nix/cmd/command.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-open.hh" +#include "nix/util/thread-pool.hh" #include @@ -105,7 +104,7 @@ struct CmdSign : StorePathsCommand .description = "File containing the secret signing key.", .labels = {"file"}, .handler = {&secretKeyFile}, - .completer = completePath + .completer = completePath, }); } @@ -175,7 +174,7 @@ struct CmdKeyGenerateSecret : Command if (!keyName) throw UsageError("required argument '--key-name' is missing"); - stopProgressBar(); + logger->stop(); writeFull(getStandardOutput(), SecretKey::generate(*keyName).to_string()); } }; @@ -197,7 +196,7 @@ struct CmdKeyConvertSecretToPublic : Command void run() override { SecretKey secretKey(drainFD(STDIN_FILENO)); - stopProgressBar(); + logger->stop(); writeFull(getStandardOutput(), secretKey.toPublicKey().to_string()); } }; diff --git a/src/nix/store-copy-log.cc b/src/nix/store-copy-log.cc index a6e8aeff7cb..599b40edc00 100644 --- a/src/nix/store-copy-log.cc +++ b/src/nix/store-copy-log.cc @@ -1,10 +1,10 @@ -#include "command.hh" -#include "shared.hh" -#include "store-api.hh" -#include "store-cast.hh" -#include "log-store.hh" -#include "sync.hh" -#include "thread-pool.hh" +#include "nix/cmd/command.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-api.hh" +#include "nix/store/store-cast.hh" +#include "nix/store/log-store.hh" +#include "nix/util/sync.hh" +#include "nix/util/thread-pool.hh" #include diff --git a/src/nix/store-delete.cc b/src/nix/store-delete.cc index 6719227dfe7..fae960c9013 100644 --- a/src/nix/store-delete.cc +++ b/src/nix/store-delete.cc @@ -1,9 +1,9 @@ -#include "command.hh" -#include "common-args.hh" -#include "shared.hh" -#include "store-api.hh" -#include "store-cast.hh" -#include "gc-store.hh" +#include "nix/cmd/command.hh" +#include "nix/main/common-args.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-api.hh" +#include "nix/store/store-cast.hh" +#include "nix/store/gc-store.hh" using namespace nix; @@ -16,7 +16,7 @@ struct CmdStoreDelete : StorePathsCommand addFlag({ .longName = "ignore-liveness", .description = "Do not check whether the paths are reachable from a root.", - .handler = {&options.ignoreLiveness, true} + .handler = {&options.ignoreLiveness, true}, }); } diff --git a/src/nix/store-gc.cc b/src/nix/store-gc.cc index 8b9b5d1642a..c71e89233b9 100644 --- a/src/nix/store-gc.cc +++ b/src/nix/store-gc.cc @@ -1,9 +1,9 @@ -#include "command.hh" -#include "common-args.hh" -#include "shared.hh" -#include "store-api.hh" -#include "store-cast.hh" -#include "gc-store.hh" +#include "nix/cmd/command.hh" +#include "nix/main/common-args.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-api.hh" +#include "nix/store/store-cast.hh" +#include "nix/store/gc-store.hh" using namespace nix; @@ -17,7 +17,7 @@ struct CmdStoreGC : StoreCommand, MixDryRun .longName = "max", .description = "Stop after freeing *n* bytes of disk space.", .labels = {"n"}, - .handler = {&options.maxFreed} + .handler = {&options.maxFreed}, }); } diff --git a/src/nix/store-info.cc b/src/nix/store-info.cc index a7c59576146..c4c63ae3a90 100644 --- a/src/nix/store-info.cc +++ b/src/nix/store-info.cc @@ -1,13 +1,13 @@ -#include "command.hh" -#include "shared.hh" -#include "store-api.hh" -#include "finally.hh" +#include "nix/cmd/command.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-api.hh" +#include "nix/util/finally.hh" #include using namespace nix; -struct CmdPingStore : StoreCommand, MixJSON +struct CmdInfoStore : StoreCommand, MixJSON { std::string description() override { @@ -33,7 +33,7 @@ struct CmdPingStore : StoreCommand, MixJSON } else { nlohmann::json res; Finally printRes([&]() { - logger->cout("%s", res); + printJSON(res); }); res["url"] = store->getUri(); @@ -46,15 +46,4 @@ struct CmdPingStore : StoreCommand, MixJSON } }; -struct CmdInfoStore : CmdPingStore -{ - void run(nix::ref store) override - { - warn("'nix store ping' is a deprecated alias for 'nix store info'"); - CmdPingStore::run(store); - } -}; - - -static auto rCmdPingStore = registerCommand2({"store", "info"}); -static auto rCmdInfoStore = registerCommand2({"store", "ping"}); +static auto rCmdInfoStore = registerCommand2({"store", "info"}); diff --git a/src/nix/store-repair.cc b/src/nix/store-repair.cc index 895e3968507..edd6999815c 100644 --- a/src/nix/store-repair.cc +++ b/src/nix/store-repair.cc @@ -1,5 +1,5 @@ -#include "command.hh" -#include "store-api.hh" +#include "nix/cmd/command.hh" +#include "nix/store/store-api.hh" using namespace nix; diff --git a/src/nix/store.cc b/src/nix/store.cc index 79b41e0965e..80f9363cade 100644 --- a/src/nix/store.cc +++ b/src/nix/store.cc @@ -1,11 +1,15 @@ -#include "command.hh" +#include "nix/cmd/command.hh" using namespace nix; struct CmdStore : NixMultiCommand { CmdStore() : NixMultiCommand("store", RegisterCommand::getCommandsFor({"store"})) - { } + { + aliases = { + {"ping", { AliasStatus::Deprecated, {"info"}}}, + }; + } std::string description() override { diff --git a/src/nix/unix/daemon.cc b/src/nix/unix/daemon.cc index 746963a0103..a14632c2f0b 100644 --- a/src/nix/unix/daemon.cc +++ b/src/nix/unix/daemon.cc @@ -1,20 +1,22 @@ ///@file -#include "signals.hh" -#include "unix-domain-socket.hh" -#include "command.hh" -#include "shared.hh" -#include "local-store.hh" -#include "remote-store.hh" -#include "remote-store-connection.hh" -#include "serialise.hh" -#include "archive.hh" -#include "globals.hh" -#include "config-global.hh" -#include "derivations.hh" -#include "finally.hh" -#include "legacy.hh" -#include "daemon.hh" +#include "nix/util/signals.hh" +#include "nix/util/unix-domain-socket.hh" +#include "nix/cmd/command.hh" +#include "nix/main/shared.hh" +#include "nix/store/local-store.hh" +#include "nix/store/remote-store.hh" +#include "nix/store/remote-store-connection.hh" +#include "nix/store/store-open.hh" +#include "nix/util/serialise.hh" +#include "nix/util/archive.hh" +#include "nix/store/globals.hh" +#include "nix/util/config-global.hh" +#include "nix/store/derivations.hh" +#include "nix/util/finally.hh" +#include "nix/cmd/legacy.hh" +#include "nix/store/daemon.hh" +#include "man-pages.hh" #include #include @@ -33,11 +35,11 @@ #include #include -#if __linux__ -#include "cgroup.hh" +#ifdef __linux__ +#include "nix/util/cgroup.hh" #endif -#if __APPLE__ || __FreeBSD__ +#if defined(__APPLE__) || defined(__FreeBSD__) #include #endif @@ -243,7 +245,7 @@ static PeerInfo getPeerInfo(int remote) */ static ref openUncachedStore() { - Store::Params params; // FIXME: get params from somewhere + Store::Config::Params params; // FIXME: get params from somewhere // Disable caching since the client already does that. params["path-info-cache-size"] = "0"; return openStore(settings.storeUri, params); @@ -316,7 +318,7 @@ static void daemonLoop(std::optional forceTrustClientOpt) // Get rid of children automatically; don't let them become zombies. setSigChldAction(true); - #if __linux__ + #ifdef __linux__ if (settings.useCgroups) { experimentalFeatureSettings.require(Xp::Cgroups); @@ -479,7 +481,7 @@ static void processStdioConnection(ref store, TrustedFlag trustClient) * @param forceTrustClientOpt See `daemonLoop()` and the parameter with * the same name over there for details. * - * @param procesOps Whether to force processing ops even if the next + * @param processOps Whether to force processing ops even if the next * store also is a remote store and could process it directly. */ static void runDaemon(bool stdio, std::optional forceTrustClientOpt, bool processOps) @@ -545,7 +547,7 @@ static int main_nix_daemon(int argc, char * * argv) static RegisterLegacyCommand r_nix_daemon("nix-daemon", main_nix_daemon); -struct CmdDaemon : StoreCommand +struct CmdDaemon : Command { bool stdio = false; std::optional isTrustedOpt = std::nullopt; @@ -570,7 +572,7 @@ struct CmdDaemon : StoreCommand addFlag({ .longName = "force-untrusted", - .description = "Force the daemon to not trust connecting clients. The connection will be processed by the receiving daemon before forwarding commands.", + .description = "Force the daemon to not trust connecting clients. The connection is processed by the receiving daemon before forwarding commands.", .handler = {[&]() { isTrustedOpt = NotTrusted; }}, @@ -614,7 +616,7 @@ struct CmdDaemon : StoreCommand ; } - void run(ref store) override + void run() override { runDaemon(stdio, isTrustedOpt, processOps); } diff --git a/src/nix/upgrade-nix.cc b/src/nix/upgrade-nix.cc index f54cc59d036..64824110460 100644 --- a/src/nix/upgrade-nix.cc +++ b/src/nix/upgrade-nix.cc @@ -1,21 +1,20 @@ -#include "processes.hh" -#include "command.hh" -#include "common-args.hh" -#include "store-api.hh" -#include "filetransfer.hh" -#include "eval.hh" -#include "eval-settings.hh" -#include "attr-path.hh" -#include "names.hh" -#include "progress-bar.hh" -#include "executable-path.hh" +#include "nix/util/processes.hh" +#include "nix/cmd/command.hh" +#include "nix/main/common-args.hh" +#include "nix/store/store-api.hh" +#include "nix/store/filetransfer.hh" +#include "nix/expr/eval.hh" +#include "nix/expr/eval-settings.hh" +#include "nix/expr/attr-path.hh" +#include "nix/store/names.hh" +#include "nix/util/executable-path.hh" #include "self-exe.hh" using namespace nix; struct CmdUpgradeNix : MixDryRun, StoreCommand { - Path profileDir; + std::filesystem::path profileDir; CmdUpgradeNix() { @@ -24,14 +23,14 @@ struct CmdUpgradeNix : MixDryRun, StoreCommand .shortName = 'p', .description = "The path to the Nix profile to upgrade.", .labels = {"profile-dir"}, - .handler = {&profileDir} + .handler = {&profileDir}, }); addFlag({ .longName = "nix-store-paths-url", .description = "The URL of the file that contains the store paths of the latest Nix release.", .labels = {"url"}, - .handler = {&(std::string&) settings.upgradeNixStorePathUrl} + .handler = {&(std::string&) settings.upgradeNixStorePathUrl}, }); } @@ -64,14 +63,14 @@ struct CmdUpgradeNix : MixDryRun, StoreCommand if (profileDir == "") profileDir = getProfileDir(store); - printInfo("upgrading Nix in profile '%s'", profileDir); + printInfo("upgrading Nix in profile %s", profileDir); auto storePath = getLatestNix(store); auto version = DrvName(storePath.name()).version; if (dryRun) { - stopProgressBar(); + logger->stop(); warn("would upgrade to version %s", version); return; } @@ -89,44 +88,48 @@ struct CmdUpgradeNix : MixDryRun, StoreCommand throw Error("could not verify that '%s' works", program); } - stopProgressBar(); + logger->stop(); { Activity act(*logger, lvlInfo, actUnknown, - fmt("installing '%s' into profile '%s'...", store->printStorePath(storePath), profileDir)); + fmt("installing '%s' into profile %s...", store->printStorePath(storePath), profileDir)); + + // FIXME: don't call an external process. runProgram(getNixBin("nix-env").string(), false, - {"--profile", profileDir, "-i", store->printStorePath(storePath), "--no-sandbox"}); + {"--profile", profileDir.string(), "-i", store->printStorePath(storePath), "--no-sandbox"}); } printInfo(ANSI_GREEN "upgrade to version %s done" ANSI_NORMAL, version); } /* Return the profile in which Nix is installed. */ - Path getProfileDir(ref store) + std::filesystem::path getProfileDir(ref store) { auto whereOpt = ExecutablePath::load().findName(OS_STR("nix-env")); if (!whereOpt) throw Error("couldn't figure out how Nix is installed, so I can't upgrade it"); - auto & where = *whereOpt; + const auto & where = whereOpt->parent_path(); - printInfo("found Nix in '%s'", where); + printInfo("found Nix in %s", where); if (hasPrefix(where.string(), "/run/current-system")) throw Error("Nix on NixOS must be upgraded via 'nixos-rebuild'"); - Path profileDir = where.parent_path().string(); + auto profileDir = where.parent_path(); // Resolve profile to /nix/var/nix/profiles/ link. - while (canonPath(profileDir).find("/profiles/") == std::string::npos && std::filesystem::is_symlink(profileDir)) - profileDir = readLink(profileDir); + while (canonPath(profileDir.string()).find("/profiles/") == std::string::npos && std::filesystem::is_symlink(profileDir)) + profileDir = readLink(profileDir.string()); + + printInfo("found profile %s", profileDir); - printInfo("found profile '%s'", profileDir); + Path userEnv = canonPath(profileDir.string(), true); - Path userEnv = canonPath(profileDir, true); + if (std::filesystem::exists(profileDir / "manifest.json")) + throw Error("directory %s is managed by 'nix profile' and currently cannot be upgraded by 'nix upgrade-nix'", profileDir); - if (where.filename() != "bin" || - !hasSuffix(userEnv, "user-environment")) - throw Error("directory '%s' does not appear to be part of a Nix profile", where); + if (!std::filesystem::exists(profileDir / "manifest.nix")) + throw Error("directory %s does not appear to be part of a Nix profile", profileDir); if (!store->isValidPath(store->parseStorePath(userEnv))) throw Error("directory '%s' is not in the Nix store", userEnv); diff --git a/src/nix/verify.cc b/src/nix/verify.cc index 52585fe08d5..eb2cde93c44 100644 --- a/src/nix/verify.cc +++ b/src/nix/verify.cc @@ -1,13 +1,13 @@ -#include "command.hh" -#include "shared.hh" -#include "store-api.hh" -#include "thread-pool.hh" -#include "signals.hh" -#include "keys.hh" +#include "nix/cmd/command.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-open.hh" +#include "nix/util/thread-pool.hh" +#include "nix/util/signals.hh" +#include "nix/store/keys.hh" #include -#include "exit.hh" +#include "nix/util/exit.hh" using namespace nix; @@ -37,7 +37,7 @@ struct CmdVerify : StorePathsCommand .shortName = 's', .description = "Use signatures from the specified store.", .labels = {"store-uri"}, - .handler = {[&](std::string s) { substituterUris.push_back(s); }} + .handler = {[&](std::string s) { substituterUris.push_back(s); }}, }); addFlag({ @@ -45,7 +45,7 @@ struct CmdVerify : StorePathsCommand .shortName = 'n', .description = "Require that each path is signed by at least *n* different keys.", .labels = {"n"}, - .handler = {&sigsNeeded} + .handler = {&sigsNeeded}, }); } diff --git a/src/nix/why-depends.cc b/src/nix/why-depends.cc index e299585ff88..3aac45d34d6 100644 --- a/src/nix/why-depends.cc +++ b/src/nix/why-depends.cc @@ -1,8 +1,7 @@ -#include "command.hh" -#include "store-api.hh" -#include "progress-bar.hh" -#include "source-accessor.hh" -#include "shared.hh" +#include "nix/cmd/command.hh" +#include "nix/store/store-api.hh" +#include "nix/util/source-accessor.hh" +#include "nix/main/shared.hh" #include @@ -110,8 +109,6 @@ struct CmdWhyDepends : SourceExprCommand, MixOperateOnOptions auto dependencyPath = *optDependencyPath; auto dependencyPathHash = dependencyPath.hashPart(); - stopProgressBar(); // FIXME - auto accessor = store->getFSAccessor(); auto const inf = std::numeric_limits::max(); @@ -175,7 +172,7 @@ struct CmdWhyDepends : SourceExprCommand, MixOperateOnOptions struct BailOut { }; printNode = [&](Node & node, const std::string & firstPad, const std::string & tailPad) { - CanonPath pathS(store->printStorePath(node.path)); + CanonPath pathS(node.path.to_string()); assert(node.dist != inf); if (precise) { @@ -196,7 +193,7 @@ struct CmdWhyDepends : SourceExprCommand, MixOperateOnOptions /* Sort the references by distance to `dependency` to ensure that the shortest path is printed first. */ std::multimap refs; - std::set hashes; + StringSet hashes; for (auto & ref : node.refs) { if (ref == node.path && packagePath != dependencyPath) continue; diff --git a/src/perl/lib/Nix/Store.xs b/src/perl/lib/Nix/Store.xs index 172c3500de0..edcb6d72a5e 100644 --- a/src/perl/lib/Nix/Store.xs +++ b/src/perl/lib/Nix/Store.xs @@ -1,6 +1,3 @@ -#include "config-util.hh" -#include "config-store.hh" - #include "EXTERN.h" #include "perl.h" #include "XSUB.h" @@ -9,11 +6,11 @@ #undef do_open #undef do_close -#include "derivations.hh" -#include "realisation.hh" -#include "globals.hh" -#include "store-api.hh" -#include "posix-source-accessor.hh" +#include "nix/store/derivations.hh" +#include "nix/store/realisation.hh" +#include "nix/store/globals.hh" +#include "nix/store/store-open.hh" +#include "nix/util/posix-source-accessor.hh" #include #include @@ -194,7 +191,7 @@ StoreWrapper::computeFSClosure(int flipDirection, int includeOutputs, ...) PPCODE: try { StorePathSet paths; - for (int n = 2; n < items; ++n) + for (int n = 3; n < items; ++n) THIS->store->computeFSClosure(THIS->store->parseStorePath(SvPV_nolen(ST(n))), paths, flipDirection, includeOutputs); for (auto & i : paths) XPUSHs(sv_2mortal(newSVpv(THIS->store->printStorePath(i).c_str(), 0))); @@ -208,7 +205,7 @@ StoreWrapper::topoSortPaths(...) PPCODE: try { StorePathSet paths; - for (int n = 0; n < items; ++n) paths.insert(THIS->store->parseStorePath(SvPV_nolen(ST(n)))); + for (int n = 1; n < items; ++n) paths.insert(THIS->store->parseStorePath(SvPV_nolen(ST(n)))); auto sorted = THIS->store->topoSortPaths(paths); for (auto & i : sorted) XPUSHs(sv_2mortal(newSVpv(THIS->store->printStorePath(i).c_str(), 0))); @@ -234,7 +231,7 @@ StoreWrapper::exportPaths(int fd, ...) PPCODE: try { StorePathSet paths; - for (int n = 1; n < items; ++n) paths.insert(THIS->store->parseStorePath(SvPV_nolen(ST(n)))); + for (int n = 2; n < items; ++n) paths.insert(THIS->store->parseStorePath(SvPV_nolen(ST(n)))); FdSink sink(fd); THIS->store->exportPaths(paths, sink); } catch (Error & e) { diff --git a/src/perl/meson.build b/src/perl/meson.build index 52d85fd6042..599e91710c4 100644 --- a/src/perl/meson.build +++ b/src/perl/meson.build @@ -57,10 +57,10 @@ libdir = join_paths(prefix, get_option('libdir')) # Required Programs #------------------------------------------------- -xz = find_program('xz') +find_program('xz') xsubpp = find_program('xsubpp') perl = find_program('perl') -curl = find_program('curl') +find_program('curl') yath = find_program('yath', required : false) # Required Libraries @@ -82,6 +82,9 @@ nix_store_dep = dependency('nix-store') # pkgconfig available, are not in a standard location, # and are installed into a version folder. Use the # Perl binary to give hints about perl include dir. +# +# Note that until we have a better solution for this, cross-compiling +# the perl bindings does not appear to be possible. #------------------------------------------------- perl_archname = run_command( perl, '-e', 'use Config; print $Config{archname};', check: true).stdout() @@ -154,7 +157,7 @@ subdir(lib_dir) if get_option('tests').enabled() yath_rc_conf = configuration_data() yath_rc_conf.set('lib_dir', lib_dir) - yath_rc = configure_file( + configure_file( output : '.yath.rc', input : '.yath.rc.in', configuration : yath_rc_conf, diff --git a/src/perl/package.nix b/src/perl/package.nix index 5ee0df13c9d..5841570cd09 100644 --- a/src/perl/package.nix +++ b/src/perl/package.nix @@ -1,76 +1,82 @@ -{ lib -, stdenv -, mkMesonDerivation -, pkg-config -, perl -, perlPackages -, nix-store -, version -, curl -, bzip2 -, libsodium +{ + lib, + stdenv, + mkMesonDerivation, + pkg-config, + perl, + perlPackages, + nix-store, + version, + curl, + bzip2, + libsodium, }: let inherit (lib) fileset; in -perl.pkgs.toPerlModule (mkMesonDerivation (finalAttrs: { - pname = "nix-perl"; - inherit version; +perl.pkgs.toPerlModule ( + mkMesonDerivation (finalAttrs: { + pname = "nix-perl"; + inherit version; - workDir = ./.; - fileset = fileset.unions ([ - ./.version - ../../.version - ./MANIFEST - ./lib - ./meson.build - ./meson.options - ] ++ lib.optionals finalAttrs.doCheck [ - ./.yath.rc.in - ./t - ]); + workDir = ./.; + fileset = fileset.unions ( + [ + ./.version + ../../.version + ./MANIFEST + ./lib + ./meson.build + ./meson.options + ] + ++ lib.optionals finalAttrs.finalPackage.doCheck [ + ./.yath.rc.in + ./t + ] + ); - nativeBuildInputs = [ - pkg-config - perl - curl - ]; + nativeBuildInputs = [ + pkg-config + perl + curl + ]; - buildInputs = [ - nix-store - ] ++ finalAttrs.passthru.externalBuildInputs; + buildInputs = [ + nix-store + ] ++ finalAttrs.passthru.externalBuildInputs; - # Hack for sake of the dev shell - passthru.externalBuildInputs = [ - bzip2 - libsodium - ]; + # Hack for sake of the dev shell + passthru.externalBuildInputs = [ + bzip2 + libsodium + ]; - # `perlPackages.Test2Harness` is marked broken for Darwin - doCheck = !stdenv.isDarwin; + # `perlPackages.Test2Harness` is marked broken for Darwin + doCheck = !stdenv.isDarwin; - nativeCheckInputs = [ - perlPackages.Test2Harness - ]; + nativeCheckInputs = [ + perlPackages.Test2Harness + ]; - preConfigure = - # "Inline" .version so its not a symlink, and includes the suffix - '' - chmod u+w .version - echo ${finalAttrs.version} > .version - ''; + preConfigure = + # "Inline" .version so its not a symlink, and includes the suffix + '' + chmod u+w .version + echo ${finalAttrs.version} > .version + ''; - mesonFlags = [ - (lib.mesonOption "dbi_path" "${perlPackages.DBI}/${perl.libPrefix}") - (lib.mesonOption "dbd_sqlite_path" "${perlPackages.DBDSQLite}/${perl.libPrefix}") - (lib.mesonEnable "tests" finalAttrs.doCheck) - ]; + mesonFlags = [ + (lib.mesonOption "dbi_path" "${perlPackages.DBI}/${perl.libPrefix}") + (lib.mesonOption "dbd_sqlite_path" "${perlPackages.DBDSQLite}/${perl.libPrefix}") + (lib.mesonEnable "tests" finalAttrs.finalPackage.doCheck) + ]; - mesonCheckFlags = [ - "--print-errorlogs" - ]; + mesonCheckFlags = [ + "--print-errorlogs" + ]; - strictDeps = false; -})) + strictDeps = false; + }) +) diff --git a/src/perl/t/meson.build b/src/perl/t/meson.build index dbd1139f327..5e75920ac0a 100644 --- a/src/perl/t/meson.build +++ b/src/perl/t/meson.build @@ -7,6 +7,7 @@ nix_perl_tests = files( 'init.t', + # hack for trailing newline ) diff --git a/tests/functional/add.sh b/tests/functional/add.sh index 3b37ee7d47f..0e6868d8f56 100755 --- a/tests/functional/add.sh +++ b/tests/functional/add.sh @@ -29,6 +29,47 @@ echo "$hash2" test "$hash1" = "sha256:$hash2" +# The contents can be accessed through a symlink, and this symlink has no effect on the hash +# https://github.com/NixOS/nix/issues/11941 +test_issue_11941() { + local expected actual + mkdir -p "$TEST_ROOT/foo/bar" && ln -s "$TEST_ROOT/foo" "$TEST_ROOT/foo-link" + + # legacy + expected=$(nix-store --add-fixed --recursive sha256 "$TEST_ROOT/foo/bar") + actual=$(nix-store --add-fixed --recursive sha256 "$TEST_ROOT/foo-link/bar") + [[ "$expected" == "$actual" ]] + actual=$(nix-store --add "$TEST_ROOT/foo-link/bar") + [[ "$expected" == "$actual" ]] + + # nix store add + actual=$(nix store add --hash-algo sha256 --mode nar "$TEST_ROOT/foo/bar") + [[ "$expected" == "$actual" ]] + + # cleanup + rm -r "$TEST_ROOT/foo" "$TEST_ROOT/foo-link" +} +test_issue_11941 + +# A symlink is added to the store as a symlink, not as a copy of the target +test_add_symlink() { + ln -s /bin "$TEST_ROOT/my-bin" + + # legacy + path=$(nix-store --add-fixed --recursive sha256 "$TEST_ROOT/my-bin") + [[ "$(readlink "$path")" == /bin ]] + path=$(nix-store --add "$TEST_ROOT/my-bin") + [[ "$(readlink "$path")" == /bin ]] + + # nix store add + path=$(nix store add --hash-algo sha256 --mode nar "$TEST_ROOT/my-bin") + [[ "$(readlink "$path")" == /bin ]] + + # cleanup + rm "$TEST_ROOT/my-bin" +} +test_add_symlink + #### New style commands clearStoreIfPossible diff --git a/tests/functional/big-derivation-attr.nix b/tests/functional/big-derivation-attr.nix index 35c1187f665..d370486d6c4 100644 --- a/tests/functional/big-derivation-attr.nix +++ b/tests/functional/big-derivation-attr.nix @@ -1,6 +1,25 @@ let sixteenBytes = "0123456789abcdef"; - times16 = s: builtins.concatStringsSep "" [s s s s s s s s s s s s s s s s]; + times16 = + s: + builtins.concatStringsSep "" [ + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + s + ]; exp = n: x: if n == 1 then x else times16 (exp (n - 1) x); sixteenMegabyte = exp 6 sixteenBytes; in diff --git a/tests/functional/binary-cache.sh b/tests/functional/binary-cache.sh index ff39ab3b7dc..2c102df0771 100755 --- a/tests/functional/binary-cache.sh +++ b/tests/functional/binary-cache.sh @@ -151,8 +151,11 @@ nix-build --substituters "file://$cacheDir" --no-require-sigs dependencies.nix - grepQuiet "don't know how to build" "$TEST_ROOT/log" grepQuiet "building.*input-1" "$TEST_ROOT/log" grepQuiet "building.*input-2" "$TEST_ROOT/log" -grepQuiet "copying path.*input-0" "$TEST_ROOT/log" -grepQuiet "copying path.*top" "$TEST_ROOT/log" + +# Removed for now since 299141ecbd08bae17013226dbeae71e842b4fdd7 / issue #77 is reverted + +#grepQuiet "copying path.*input-0" "$TEST_ROOT/log" +#grepQuiet "copying path.*top" "$TEST_ROOT/log" # Create a signed binary cache. diff --git a/tests/functional/build-cores.nix b/tests/functional/build-cores.nix new file mode 100644 index 00000000000..9b763f7d866 --- /dev/null +++ b/tests/functional/build-cores.nix @@ -0,0 +1,11 @@ +with import ./config.nix; + +{ + # Test derivation that checks the NIX_BUILD_CORES environment variable + testCores = mkDerivation { + name = "test-build-cores"; + buildCommand = '' + echo "$NIX_BUILD_CORES" > $out + ''; + }; +} diff --git a/tests/functional/build-cores.sh b/tests/functional/build-cores.sh new file mode 100755 index 00000000000..a226774c6a7 --- /dev/null +++ b/tests/functional/build-cores.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +source common.sh + +clearStoreIfPossible + +echo "Testing build-cores configuration behavior..." + +# Test 1: When build-cores is set to a non-zero value, NIX_BUILD_CORES should have that value +echo "Testing build-cores=4..." +rm -f "$TEST_ROOT"/build-cores-output +nix-build --cores 4 build-cores.nix -A testCores -o "$TEST_ROOT"/build-cores-output +result=$(cat "$(readlink "$TEST_ROOT"/build-cores-output)") +if [[ "$result" != "4" ]]; then + echo "FAIL: Expected NIX_BUILD_CORES=4, got $result" + exit 1 +fi +echo "PASS: build-cores=4 correctly sets NIX_BUILD_CORES=4" +rm -f "$TEST_ROOT"/build-cores-output + +# Test 2: When build-cores is set to 0, NIX_BUILD_CORES should be resolved to getDefaultCores() +echo "Testing build-cores=0..." +nix-build --cores 0 build-cores.nix -A testCores -o "$TEST_ROOT"/build-cores-output +result=$(cat "$(readlink "$TEST_ROOT"/build-cores-output)") +if [[ "$result" == "0" ]]; then + echo "FAIL: NIX_BUILD_CORES should not be 0 when build-cores=0" + exit 1 +fi +echo "PASS: build-cores=0 resolves to NIX_BUILD_CORES=$result (should be > 0)" +rm -f "$TEST_ROOT"/build-cores-output + +echo "All build-cores tests passed!" diff --git a/tests/functional/build-hook-ca-fixed.nix b/tests/functional/build-hook-ca-fixed.nix index 0ce6d9b128b..3d2643c1321 100644 --- a/tests/functional/build-hook-ca-fixed.nix +++ b/tests/functional/build-hook-ca-fixed.nix @@ -4,24 +4,39 @@ with import ./config.nix; let - mkDerivation = args: - derivation ({ - inherit system; - builder = busybox; - args = ["sh" "-e" args.builder or (builtins.toFile "builder-${args.name}.sh" '' - if [ -e "$NIX_ATTRS_SH_FILE" ]; then source $NIX_ATTRS_SH_FILE; fi; - eval "$buildCommand" - '')]; - outputHashMode = "recursive"; - outputHashAlgo = "sha256"; - } // removeAttrs args ["builder" "meta" "passthru"]) - // { meta = args.meta or {}; passthru = args.passthru or {}; }; + mkDerivation = + args: + derivation ( + { + inherit system; + builder = busybox; + args = [ + "sh" + "-e" + args.builder or (builtins.toFile "builder-${args.name}.sh" '' + if [ -e "$NIX_ATTRS_SH_FILE" ]; then source $NIX_ATTRS_SH_FILE; fi; + eval "$buildCommand" + '') + ]; + outputHashMode = "recursive"; + outputHashAlgo = "sha256"; + } + // removeAttrs args [ + "builder" + "meta" + "passthru" + ] + ) + // { + meta = args.meta or { }; + passthru = args.passthru or { }; + }; input1 = mkDerivation { shell = busybox; name = "build-remote-input-1"; buildCommand = "echo hi-input1; echo FOO > $out"; - requiredSystemFeatures = ["foo"]; + requiredSystemFeatures = [ "foo" ]; outputHash = "sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc="; }; @@ -29,7 +44,7 @@ let shell = busybox; name = "build-remote-input-2"; buildCommand = "echo hi; echo BAR > $out"; - requiredSystemFeatures = ["bar"]; + requiredSystemFeatures = [ "bar" ]; outputHash = "sha256-XArauVH91AVwP9hBBQNlkX9ccuPpSYx9o0zeIHb6e+Q="; }; @@ -41,21 +56,20 @@ let read x < ${input2} echo $x BAZ > $out ''; - requiredSystemFeatures = ["baz"]; + requiredSystemFeatures = [ "baz" ]; outputHash = "sha256-daKAcPp/+BYMQsVi/YYMlCKoNAxCNDsaivwSHgQqD2s="; }; in - mkDerivation { - shell = busybox; - name = "build-remote"; - passthru = { inherit input1 input2 input3; }; - buildCommand = - '' - read x < ${input1} - read y < ${input3} - echo "$x $y" > $out - ''; - outputHash = "sha256-5SxbkUw6xe2l9TE1uwCvTtTDysD1vhRor38OtDF0LqQ="; - } +mkDerivation { + shell = busybox; + name = "build-remote"; + passthru = { inherit input1 input2 input3; }; + buildCommand = '' + read x < ${input1} + read y < ${input3} + echo "$x $y" > $out + ''; + outputHash = "sha256-5SxbkUw6xe2l9TE1uwCvTtTDysD1vhRor38OtDF0LqQ="; +} diff --git a/tests/functional/build-hook.nix b/tests/functional/build-hook.nix index 99a13aee483..45a2a84d6d4 100644 --- a/tests/functional/build-hook.nix +++ b/tests/functional/build-hook.nix @@ -1,39 +1,61 @@ -{ busybox, contentAddressed ? false }: +{ + busybox, + contentAddressed ? false, +}: with import ./config.nix; let - caArgs = if contentAddressed then { - outputHashMode = "recursive"; - outputHashAlgo = "sha256"; - __contentAddressed = true; - } else {}; - - mkDerivation = args: - derivation ({ - inherit system; - builder = busybox; - args = ["sh" "-e" args.builder or (builtins.toFile "builder-${args.name}.sh" '' - if [ -e "$NIX_ATTRS_SH_FILE" ]; then source $NIX_ATTRS_SH_FILE; fi; - eval "$buildCommand" - '')]; - } // removeAttrs args ["builder" "meta" "passthru"] - // caArgs) - // { meta = args.meta or {}; passthru = args.passthru or {}; }; + caArgs = + if contentAddressed then + { + outputHashMode = "recursive"; + outputHashAlgo = "sha256"; + __contentAddressed = true; + } + else + { }; + + mkDerivation = + args: + derivation ( + { + inherit system; + builder = busybox; + args = [ + "sh" + "-e" + args.builder or (builtins.toFile "builder-${args.name}.sh" '' + if [ -e "$NIX_ATTRS_SH_FILE" ]; then source $NIX_ATTRS_SH_FILE; fi; + eval "$buildCommand" + '') + ]; + } + // removeAttrs args [ + "builder" + "meta" + "passthru" + ] + // caArgs + ) + // { + meta = args.meta or { }; + passthru = args.passthru or { }; + }; input1 = mkDerivation { shell = busybox; name = "build-remote-input-1"; buildCommand = "echo hi-input1; echo FOO > $out"; - requiredSystemFeatures = ["foo"]; + requiredSystemFeatures = [ "foo" ]; }; input2 = mkDerivation { shell = busybox; name = "build-remote-input-2"; buildCommand = "echo hi; echo BAR > $out"; - requiredSystemFeatures = ["bar"]; + requiredSystemFeatures = [ "bar" ]; }; input3 = mkDerivation { @@ -44,19 +66,18 @@ let read x < ${input2} echo $x BAZ > $out ''; - requiredSystemFeatures = ["baz"]; + requiredSystemFeatures = [ "baz" ]; }; in - mkDerivation { - shell = busybox; - name = "build-remote"; - passthru = { inherit input1 input2 input3; }; - buildCommand = - '' - read x < ${input1} - read y < ${input3} - echo "$x $y" > $out - ''; - } +mkDerivation { + shell = busybox; + name = "build-remote"; + passthru = { inherit input1 input2 input3; }; + buildCommand = '' + read x < ${input1} + read y < ${input3} + echo "$x $y" > $out + ''; +} diff --git a/tests/functional/build-remote-content-addressed-floating.sh b/tests/functional/build-remote-content-addressed-floating.sh index 33d667f9211..37091590573 100755 --- a/tests/functional/build-remote-content-addressed-floating.sh +++ b/tests/functional/build-remote-content-addressed-floating.sh @@ -6,6 +6,6 @@ file=build-hook-ca-floating.nix enableFeatures "ca-derivations" -CONTENT_ADDRESSED=true +NIX_TESTS_CA_BY_DEFAULT=true source build-remote.sh diff --git a/tests/functional/build-remote-trustless-should-fail-0.sh b/tests/functional/build-remote-trustless-should-fail-0.sh index 4eccb73e0ee..e79527d7290 100755 --- a/tests/functional/build-remote-trustless-should-fail-0.sh +++ b/tests/functional/build-remote-trustless-should-fail-0.sh @@ -8,10 +8,10 @@ TODO_NixOS restartDaemon requireSandboxSupport +requiresUnprivilegedUserNamespaces [[ $busybox =~ busybox ]] || skipTest "no busybox" unset NIX_STORE_DIR -unset NIX_STATE_DIR # We first build a dependency of the derivation we eventually want to # build. diff --git a/tests/functional/build-remote-trustless.sh b/tests/functional/build-remote-trustless.sh index c498d46c301..6014b57bb1e 100644 --- a/tests/functional/build-remote-trustless.sh +++ b/tests/functional/build-remote-trustless.sh @@ -5,10 +5,10 @@ # shellcheck disable=SC2154 requireSandboxSupport +requiresUnprivilegedUserNamespaces [[ "$busybox" =~ busybox ]] || skipTest "no busybox" unset NIX_STORE_DIR -unset NIX_STATE_DIR remoteDir=$TEST_ROOT/remote diff --git a/tests/functional/build-remote.sh b/tests/functional/build-remote.sh index 1a53345778d..f396bc72e8f 100644 --- a/tests/functional/build-remote.sh +++ b/tests/functional/build-remote.sh @@ -3,16 +3,16 @@ : "${file?must be defined by caller (remote building test case using this)}" requireSandboxSupport +requiresUnprivilegedUserNamespaces [[ "${busybox-}" =~ busybox ]] || skipTest "no busybox" # Avoid store dir being inside sandbox build-dir unset NIX_STORE_DIR -unset NIX_STATE_DIR function join_by { local d=$1; shift; echo -n "$1"; shift; printf "%s" "${@/#/$d}"; } EXTRA_SYSTEM_FEATURES=() -if [[ -n "${CONTENT_ADDRESSED-}" ]]; then +if [[ -n "${NIX_TESTS_CA_BY_DEFAULT-}" ]]; then EXTRA_SYSTEM_FEATURES=("ca-derivations") fi @@ -27,6 +27,7 @@ builders=( chmod -R +w "$TEST_ROOT/machine"* || true rm -rf "$TEST_ROOT/machine"* || true + # Note: ssh://localhost bypasses ssh, directly invoking nix-store as a # child process. This allows us to test LegacySSHStore::buildDerivation(). # ssh-ng://... likewise allows us to test RemoteStore::buildDerivation(). @@ -83,6 +84,7 @@ out="$(nix-build 2>&1 failing.nix \ --arg busybox "$busybox")" || true [[ "$out" =~ .*"note: keeping build directory".* ]] +[[ "$out" =~ .*"The failed build directory was kept on the remote builder due to".* ]] build_dir="$(grep "note: keeping build" <<< "$out" | sed -E "s/^(.*)note: keeping build directory '(.*)'(.*)$/\2/")" [[ "foo" = $(<"$build_dir"/bar) ]] diff --git a/tests/functional/build.sh b/tests/functional/build.sh index 3f65a7c2cc0..0a19ff7dabb 100755 --- a/tests/functional/build.sh +++ b/tests/functional/build.sh @@ -179,12 +179,23 @@ test "$(<<<"$out" grep -cE '^error:')" = 4 out="$(nix build -f fod-failing.nix -L x4 2>&1)" && status=0 || status=$? test "$status" = 1 test "$(<<<"$out" grep -cE '^error:')" = 2 -<<<"$out" grepQuiet -E "error: 1 dependencies of derivation '.*-x4\\.drv' failed to build" + +if isDaemonNewer "2.29pre"; then + <<<"$out" grepQuiet -E "error: Cannot build '.*-x4\\.drv'" + <<<"$out" grepQuiet -E "Reason: 1 dependency failed." +else + <<<"$out" grepQuiet -E "error: 1 dependencies of derivation '.*-x4\\.drv' failed to build" +fi <<<"$out" grepQuiet -E "hash mismatch in fixed-output derivation '.*-x2\\.drv'" out="$(nix build -f fod-failing.nix -L x4 --keep-going 2>&1)" && status=0 || status=$? test "$status" = 1 test "$(<<<"$out" grep -cE '^error:')" = 3 -<<<"$out" grepQuiet -E "error: 2 dependencies of derivation '.*-x4\\.drv' failed to build" +if isDaemonNewer "2.29pre"; then + <<<"$out" grepQuiet -E "error: Cannot build '.*-x4\\.drv'" + <<<"$out" grepQuiet -E "Reason: 2 dependencies failed." +else + <<<"$out" grepQuiet -E "error: 2 dependencies of derivation '.*-x4\\.drv' failed to build" +fi <<<"$out" grepQuiet -vE "hash mismatch in fixed-output derivation '.*-x3\\.drv'" <<<"$out" grepQuiet -vE "hash mismatch in fixed-output derivation '.*-x2\\.drv'" diff --git a/tests/functional/ca-shell.nix b/tests/functional/ca-shell.nix index 36e1d1526f3..69ce6b6f17e 100644 --- a/tests/functional/ca-shell.nix +++ b/tests/functional/ca-shell.nix @@ -1 +1,5 @@ -{ inNixShell ? false, ... }@args: import ./shell.nix (args // { contentAddressed = true; }) +{ + inNixShell ? false, + ... +}@args: +import ./shell.nix (args // { contentAddressed = true; }) diff --git a/tests/functional/ca/content-addressed.nix b/tests/functional/ca/content-addressed.nix index 2559c562f92..e15208491d2 100644 --- a/tests/functional/ca/content-addressed.nix +++ b/tests/functional/ca/content-addressed.nix @@ -1,14 +1,22 @@ with import ./config.nix; -let mkCADerivation = args: mkDerivation ({ - __contentAddressed = true; - outputHashMode = "recursive"; - outputHashAlgo = "sha256"; -} // args); +let + mkCADerivation = + args: + mkDerivation ( + { + __contentAddressed = true; + outputHashMode = "recursive"; + outputHashAlgo = "sha256"; + } + // args + ); in -{ seed ? 0 }: -# A simple content-addressed derivation. +{ + seed ? 0, +}: +# A simple content-addressing derivation. # The derivation can be arbitrarily modified by passing a different `seed`, # but the output will always be the same rec { @@ -23,7 +31,11 @@ rec { }; rootCA = mkCADerivation { name = "rootCA"; - outputs = [ "out" "dev" "foo" ]; + outputs = [ + "out" + "dev" + "foo" + ]; buildCommand = '' echo "building a CA derivation" echo "The seed is ${toString seed}" diff --git a/tests/functional/ca/derivation-advanced-attributes.sh b/tests/functional/ca/derivation-advanced-attributes.sh new file mode 100755 index 00000000000..b70463e5c48 --- /dev/null +++ b/tests/functional/ca/derivation-advanced-attributes.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +export NIX_TESTS_CA_BY_DEFAULT=1 + +cd .. +source derivation-advanced-attributes.sh diff --git a/tests/functional/ca/derivation-json.sh b/tests/functional/ca/derivation-json.sh index bd6dd7177c6..0b8bcac0cc8 100644 --- a/tests/functional/ca/derivation-json.sh +++ b/tests/functional/ca/derivation-json.sh @@ -12,7 +12,7 @@ drvPath2=$(nix derivation add < "$TEST_HOME"/simple.json) [[ "$drvPath" = "$drvPath2" ]] -# Content-addressed derivations can be renamed. +# Content-addressing derivations can be renamed. jq '.name = "foo"' < "$TEST_HOME"/simple.json > "$TEST_HOME"/foo.json drvPath3=$(nix derivation add --dry-run < "$TEST_HOME"/foo.json) # With --dry-run nothing is actually written diff --git a/tests/functional/ca/flake.nix b/tests/functional/ca/flake.nix index 332c92a6792..28a27c4b31d 100644 --- a/tests/functional/ca/flake.nix +++ b/tests/functional/ca/flake.nix @@ -1,3 +1,3 @@ { - outputs = { self }: import ./content-addressed.nix {}; + outputs = { self }: import ./content-addressed.nix { }; } diff --git a/tests/functional/ca/meson.build b/tests/functional/ca/meson.build index 7a7fcc5cf6f..a4611ca4200 100644 --- a/tests/functional/ca/meson.build +++ b/tests/functional/ca/meson.build @@ -8,10 +8,11 @@ suites += { 'name': 'ca', 'deps': [], 'tests': [ + 'build-cache.sh', 'build-with-garbage-path.sh', 'build.sh', - 'build-cache.sh', 'concurrent-builds.sh', + 'derivation-advanced-attributes.sh', 'derivation-json.sh', 'duplicate-realisation-in-closure.sh', 'eval-store.sh', diff --git a/tests/functional/ca/nix-shell.sh b/tests/functional/ca/nix-shell.sh index d1fbe54d19d..7b30b2ac858 100755 --- a/tests/functional/ca/nix-shell.sh +++ b/tests/functional/ca/nix-shell.sh @@ -2,6 +2,6 @@ source common.sh -CONTENT_ADDRESSED=true +NIX_TESTS_CA_BY_DEFAULT=true cd .. source ./nix-shell.sh diff --git a/tests/functional/ca/nondeterministic.nix b/tests/functional/ca/nondeterministic.nix index d6d099a3e0e..2af26f0ac2e 100644 --- a/tests/functional/ca/nondeterministic.nix +++ b/tests/functional/ca/nondeterministic.nix @@ -1,10 +1,16 @@ with import ./config.nix; -let mkCADerivation = args: mkDerivation ({ - __contentAddressed = true; - outputHashMode = "recursive"; - outputHashAlgo = "sha256"; -} // args); +let + mkCADerivation = + args: + mkDerivation ( + { + __contentAddressed = true; + outputHashMode = "recursive"; + outputHashAlgo = "sha256"; + } + // args + ); in rec { @@ -15,13 +21,15 @@ rec { echo $(date) > $out/current-time ''; }; - dep = seed: mkCADerivation { - name = "dep"; - inherit seed; - buildCommand = '' - echo ${currentTime} > $out - ''; - }; + dep = + seed: + mkCADerivation { + name = "dep"; + inherit seed; + buildCommand = '' + echo ${currentTime} > $out + ''; + }; dep1 = dep 1; dep2 = dep 2; toplevel = mkCADerivation { @@ -32,4 +40,3 @@ rec { ''; }; } - diff --git a/tests/functional/ca/racy.nix b/tests/functional/ca/racy.nix index 555a1548464..cbc0e1643a7 100644 --- a/tests/functional/ca/racy.nix +++ b/tests/functional/ca/racy.nix @@ -1,7 +1,6 @@ # A derivation that would certainly fail if several builders tried to # build it at once. - with import ./config.nix; mkDerivation { diff --git a/tests/functional/characterisation/framework.sh b/tests/functional/characterisation/framework.sh index 5ca125ab5bc..d2c2155db80 100644 --- a/tests/functional/characterisation/framework.sh +++ b/tests/functional/characterisation/framework.sh @@ -1,5 +1,7 @@ # shellcheck shell=bash +badTestNames=() + # Golden test support # # Test that the output of the given test matches what is expected. If @@ -18,10 +20,11 @@ function diffAndAcceptInner() { fi # Diff so we get a nice message - if ! diff --color=always --unified "$expectedOrEmpty" "$got"; then - echo "FAIL: evaluation result of $testName not as expected" + if ! diff >&2 --color=always --unified "$expectedOrEmpty" "$got"; then + echo >&2 "FAIL: evaluation result of $testName not as expected" # shellcheck disable=SC2034 badDiff=1 + badTestNames+=("$testName") fi # Update expected if `_NIX_TEST_ACCEPT` is non-empty. @@ -42,14 +45,14 @@ function characterisationTestExit() { if test -n "${_NIX_TEST_ACCEPT-}"; then if (( "$badDiff" )); then set +x - echo 'Output did mot match, but accepted output as the persisted expected output.' - echo 'That means the next time the tests are run, they should pass.' + echo >&2 'Output did mot match, but accepted output as the persisted expected output.' + echo >&2 'That means the next time the tests are run, they should pass.' set -x else set +x - echo 'NOTE: Environment variable _NIX_TEST_ACCEPT is defined,' - echo 'indicating the unexpected output should be accepted as the expected output going forward,' - echo 'but no tests had unexpected output so there was no expected output to update.' + echo >&2 'NOTE: Environment variable _NIX_TEST_ACCEPT is defined,' + echo >&2 'indicating the unexpected output should be accepted as the expected output going forward,' + echo >&2 'but no tests had unexpected output so there was no expected output to update.' set -x fi if (( "$badExitCode" )); then @@ -60,16 +63,21 @@ function characterisationTestExit() { else if (( "$badDiff" )); then set +x - echo '' - echo 'You can rerun this test with:' - echo '' - echo " _NIX_TEST_ACCEPT=1 make tests/functional/${TEST_NAME}.sh.test" - echo '' - echo 'to regenerate the files containing the expected output,' - echo 'and then view the git diff to decide whether a change is' - echo 'good/intentional or bad/unintentional.' - echo 'If the diff contains arbitrary or impure information,' - echo 'please improve the normalization that the test applies to the output.' + echo >&2 '' + echo >&2 'The following tests had unexpected output:' + for testName in "${badTestNames[@]}"; do + echo >&2 " $testName" + done + echo >&2 '' + echo >&2 'You can rerun this test with:' + echo >&2 '' + echo >&2 " _NIX_TEST_ACCEPT=1 meson test ${TEST_NAME}" + echo >&2 '' + echo >&2 'to regenerate the files containing the expected output,' + echo >&2 'and then view the git diff to decide whether a change is' + echo >&2 'good/intentional or bad/unintentional.' + echo >&2 'If the diff contains arbitrary or impure information,' + echo >&2 'please improve the normalization that the test applies to the output.' set -x fi exit $(( "$badExitCode" + "$badDiff" )) diff --git a/tests/functional/check-refs.nix b/tests/functional/check-refs.nix index 89690e456c1..bdd5c4f8dc3 100644 --- a/tests/functional/check-refs.nix +++ b/tests/functional/check-refs.nix @@ -2,11 +2,16 @@ with import ./config.nix; rec { - dep = import ./dependencies.nix {}; + dep = import ./dependencies.nix { }; - makeTest = nr: args: mkDerivation ({ - name = "check-refs-" + toString nr; - } // args); + makeTest = + nr: args: + mkDerivation ( + { + name = "check-refs-" + toString nr; + } + // args + ); src = builtins.toFile "aux-ref" "bla bla"; @@ -22,31 +27,31 @@ rec { test3 = makeTest 3 { builder = builtins.toFile "builder.sh" "mkdir $out; ln -s $dep $out/link"; - allowedReferences = []; + allowedReferences = [ ]; inherit dep; }; test4 = makeTest 4 { builder = builtins.toFile "builder.sh" "mkdir $out; ln -s $dep $out/link"; - allowedReferences = [dep]; + allowedReferences = [ dep ]; inherit dep; }; test5 = makeTest 5 { builder = builtins.toFile "builder.sh" "mkdir $out"; - allowedReferences = []; + allowedReferences = [ ]; inherit dep; }; test6 = makeTest 6 { builder = builtins.toFile "builder.sh" "mkdir $out; ln -s $out $out/link"; - allowedReferences = []; + allowedReferences = [ ]; inherit dep; }; test7 = makeTest 7 { builder = builtins.toFile "builder.sh" "mkdir $out; ln -s $out $out/link"; - allowedReferences = ["out"]; + allowedReferences = [ "out" ]; inherit dep; }; @@ -58,20 +63,29 @@ rec { test9 = makeTest 9 { builder = builtins.toFile "builder.sh" "mkdir $out; ln -s $dep $out/link"; inherit dep; - disallowedReferences = [dep]; + disallowedReferences = [ dep ]; }; test10 = makeTest 10 { builder = builtins.toFile "builder.sh" "mkdir $out; echo $test5; ln -s $dep $out/link"; inherit dep test5; - disallowedReferences = [test5]; + disallowedReferences = [ test5 ]; }; test11 = makeTest 11 { __structuredAttrs = true; unsafeDiscardReferences.out = true; - outputChecks.out.allowedReferences = []; + outputChecks.out.allowedReferences = [ ]; buildCommand = ''echo ${dep} > "''${outputs[out]}"''; }; + test12 = makeTest 12 { + builder = builtins.toFile "builder.sh" "mkdir $out $lib"; + outputs = [ + "out" + "lib" + ]; + disallowedReferences = [ "dev" ]; + }; + } diff --git a/tests/functional/check-refs.sh b/tests/functional/check-refs.sh index 5c3ac915ecf..590c3fb536f 100755 --- a/tests/functional/check-refs.sh +++ b/tests/functional/check-refs.sh @@ -60,3 +60,9 @@ if ! isTestOnNixOS; then fi fi + +if isDaemonNewer "2.28pre20241225"; then + # test12 should fail (syntactically invalid). + expectStderr 1 nix-build -vvv -o "$RESULT" check-refs.nix -A test12 >"$TEST_ROOT/test12.stderr" + grepQuiet -F "output check for 'lib' contains an illegal reference specifier 'dev', expected store path or output name (one of [lib, out])" < "$TEST_ROOT/test12.stderr" +fi diff --git a/tests/functional/check-reqs.nix b/tests/functional/check-reqs.nix index 41436cb48e0..3cca761846a 100644 --- a/tests/functional/check-reqs.nix +++ b/tests/functional/check-reqs.nix @@ -22,36 +22,48 @@ rec { ''; }; - makeTest = nr: allowreqs: mkDerivation { - name = "check-reqs-" + toString nr; - inherit deps; - builder = builtins.toFile "builder.sh" '' - mkdir $out - ln -s $deps $out/depdir1 - ''; - allowedRequisites = allowreqs; - }; + makeTest = + nr: allowreqs: + mkDerivation { + name = "check-reqs-" + toString nr; + inherit deps; + builder = builtins.toFile "builder.sh" '' + mkdir $out + ln -s $deps $out/depdir1 + ''; + allowedRequisites = allowreqs; + }; # When specifying all the requisites, the build succeeds. - test1 = makeTest 1 [ dep1 dep2 deps ]; + test1 = makeTest 1 [ + dep1 + dep2 + deps + ]; # But missing anything it fails. - test2 = makeTest 2 [ dep2 deps ]; - test3 = makeTest 3 [ dep1 deps ]; + test2 = makeTest 2 [ + dep2 + deps + ]; + test3 = makeTest 3 [ + dep1 + deps + ]; test4 = makeTest 4 [ deps ]; - test5 = makeTest 5 []; + test5 = makeTest 5 [ ]; test6 = mkDerivation { name = "check-reqs"; inherit deps; builder = builtins.toFile "builder.sh" "mkdir $out; ln -s $deps $out/depdir1"; - disallowedRequisites = [dep1]; + disallowedRequisites = [ dep1 ]; }; test7 = mkDerivation { name = "check-reqs"; inherit deps; builder = builtins.toFile "builder.sh" "mkdir $out; ln -s $deps $out/depdir1"; - disallowedRequisites = [test1]; + disallowedRequisites = [ test1 ]; }; } diff --git a/tests/functional/check.nix b/tests/functional/check.nix index ddab8eea9cb..d83c28ca2ee 100644 --- a/tests/functional/check.nix +++ b/tests/functional/check.nix @@ -1,4 +1,6 @@ -{checkBuildId ? 0}: +{ + checkBuildId ? 0, +}: with import ./config.nix; @@ -6,41 +8,38 @@ with import ./config.nix; nondeterministic = mkDerivation { inherit checkBuildId; name = "nondeterministic"; - buildCommand = - '' - mkdir $out - date +%s.%N > $out/date - echo "CHECK_TMPDIR=$TMPDIR" - echo "checkBuildId=$checkBuildId" - echo "$checkBuildId" > $TMPDIR/checkBuildId - ''; + buildCommand = '' + mkdir $out + date +%s.%N > $out/date + echo "CHECK_TMPDIR=$TMPDIR" + echo "checkBuildId=$checkBuildId" + echo "$checkBuildId" > $TMPDIR/checkBuildId + ''; }; deterministic = mkDerivation { inherit checkBuildId; name = "deterministic"; - buildCommand = - '' - mkdir $out - echo date > $out/date - echo "CHECK_TMPDIR=$TMPDIR" - echo "checkBuildId=$checkBuildId" - echo "$checkBuildId" > $TMPDIR/checkBuildId - ''; + buildCommand = '' + mkdir $out + echo date > $out/date + echo "CHECK_TMPDIR=$TMPDIR" + echo "checkBuildId=$checkBuildId" + echo "$checkBuildId" > $TMPDIR/checkBuildId + ''; }; failed = mkDerivation { inherit checkBuildId; name = "failed"; - buildCommand = - '' - mkdir $out - echo date > $out/date - echo "CHECK_TMPDIR=$TMPDIR" - echo "checkBuildId=$checkBuildId" - echo "$checkBuildId" > $TMPDIR/checkBuildId - false - ''; + buildCommand = '' + mkdir $out + echo date > $out/date + echo "CHECK_TMPDIR=$TMPDIR" + echo "checkBuildId=$checkBuildId" + echo "$checkBuildId" > $TMPDIR/checkBuildId + false + ''; }; hashmismatch = import { diff --git a/tests/functional/check.sh b/tests/functional/check.sh index b213492889f..a1c6decf5b5 100755 --- a/tests/functional/check.sh +++ b/tests/functional/check.sh @@ -22,6 +22,11 @@ clearStore nix-build dependencies.nix --no-out-link nix-build dependencies.nix --no-out-link --check +# Make sure checking just one output works (#13293) +nix-build multiple-outputs.nix -A a --no-out-link +nix-store --delete "$(nix-build multiple-outputs.nix -A a.second --no-out-link)" +nix-build multiple-outputs.nix -A a.first --no-out-link --check + # Build failure exit codes (100, 104, etc.) are from # doc/manual/source/command-ref/status-build-failure.md diff --git a/tests/functional/chroot-store.sh b/tests/functional/chroot-store.sh index 46e91f0aaea..7300f04ba75 100755 --- a/tests/functional/chroot-store.sh +++ b/tests/functional/chroot-store.sh @@ -2,6 +2,28 @@ source common.sh +# Regression test for #11503. +mkdir -p "$TEST_ROOT/directory" +cat > "$TEST_ROOT/directory/default.nix" < "$TEST_ROOT"/example.txt mkdir -p "$TEST_ROOT/x" @@ -40,6 +62,7 @@ EOF cp simple.nix shell.nix simple.builder.sh "${config_nix}" "$flakeDir/" TODO_NixOS + requiresUnprivilegedUserNamespaces outPath=$(nix build --print-out-paths --no-link --sandbox-paths '/nix? /bin? /lib? /lib64? /usr?' --store "$TEST_ROOT/x" path:"$flakeDir") diff --git a/tests/functional/common/functions.sh b/tests/functional/common/functions.sh index 7195149cbfc..1b2ec8fe0e8 100644 --- a/tests/functional/common/functions.sh +++ b/tests/functional/common/functions.sh @@ -67,7 +67,7 @@ startDaemon() { die "startDaemon: not supported when testing on NixOS. Is it really needed? If so add conditionals; e.g. if ! isTestOnNixOS; then ..." fi - # Don’t start the daemon twice, as this would just make it loop indefinitely + # Don't start the daemon twice, as this would just make it loop indefinitely. if [[ "${_NIX_TEST_DAEMON_PID-}" != '' ]]; then return fi @@ -76,15 +76,19 @@ startDaemon() { PATH=$DAEMON_PATH nix --extra-experimental-features 'nix-command' daemon & _NIX_TEST_DAEMON_PID=$! export _NIX_TEST_DAEMON_PID - for ((i = 0; i < 300; i++)); do + for ((i = 0; i < 60; i++)); do if [[ -S $NIX_DAEMON_SOCKET_PATH ]]; then DAEMON_STARTED=1 break; fi + if ! kill -0 "$_NIX_TEST_DAEMON_PID"; then + echo "daemon died unexpectedly" >&2 + exit 1 + fi sleep 0.1 done if [[ -z ${DAEMON_STARTED+x} ]]; then - fail "Didn’t manage to start the daemon" + fail "Didn't manage to start the daemon" fi trap "killDaemon" EXIT # Save for if daemon is killed @@ -97,7 +101,7 @@ killDaemon() { die "killDaemon: not supported when testing on NixOS. Is it really needed? If so add conditionals; e.g. if ! isTestOnNixOS; then ..." fi - # Don’t fail trying to stop a non-existant daemon twice + # Don't fail trying to stop a non-existant daemon twice. if [[ "${_NIX_TEST_DAEMON_PID-}" == '' ]]; then return fi @@ -219,7 +223,7 @@ assertStderr() { needLocalStore() { if [[ "$NIX_REMOTE" == "daemon" ]]; then - skipTest "Can’t run through the daemon ($1)" + skipTest "Can't run through the daemon ($1)" fi } @@ -345,4 +349,15 @@ count() { trap onError ERR +requiresUnprivilegedUserNamespaces() { + if [[ -f /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]] && [[ $(< /proc/sys/kernel/apparmor_restrict_unprivileged_userns) -eq 1 ]]; then + skipTest "Unprivileged user namespaces are disabled. Run 'sudo sysctl -w /proc/sys/kernel/apparmor_restrict_unprivileged_userns=0' to allow, and run these tests." + fi +} + +execUnshare () { + requiresUnprivilegedUserNamespaces + exec unshare --mount --map-root-user "$SHELL" "$@" +} + fi # COMMON_FUNCTIONS_SH_SOURCED diff --git a/tests/functional/common/vars.sh b/tests/functional/common/vars.sh index 4b88e852618..ed4b477278f 100644 --- a/tests/functional/common/vars.sh +++ b/tests/functional/common/vars.sh @@ -60,6 +60,7 @@ unset XDG_DATA_HOME unset XDG_CONFIG_HOME unset XDG_CONFIG_DIRS unset XDG_CACHE_HOME +unset GIT_DIR export IMPURE_VAR1=foo export IMPURE_VAR2=bar diff --git a/tests/functional/dependencies.nix b/tests/functional/dependencies.nix index be1a7ae9a6e..570ea743135 100644 --- a/tests/functional/dependencies.nix +++ b/tests/functional/dependencies.nix @@ -1,7 +1,9 @@ -{ hashInvalidator ? "" }: +{ + hashInvalidator ? "", +}: with import ./config.nix; -let { +let input0 = mkDerivation { name = "dependencies-input-0"; @@ -33,16 +35,15 @@ let { outputHash = "1dq9p0hnm1y75q2x40fws5887bq1r840hzdxak0a9djbwvx0b16d"; }; - body = mkDerivation { - name = "dependencies-top"; - builder = ./dependencies.builder0.sh + "/FOOBAR/../."; - input1 = input1 + "/."; - input2 = "${input2}/."; - input1_drv = input1; - input2_drv = input2; - input0_drv = input0; - fod_input_drv = fod_input; - meta.description = "Random test package"; - }; - +in +mkDerivation { + name = "dependencies-top"; + builder = ./dependencies.builder0.sh + "/FOOBAR/../."; + input1 = input1 + "/."; + input2 = "${input2}/."; + input1_drv = input1; + input2_drv = input2; + input0_drv = input0; + fod_input_drv = fod_input; + meta.description = "Random test package"; } diff --git a/tests/functional/derivation-advanced-attributes.sh b/tests/functional/derivation-advanced-attributes.sh index 271f17dc69c..a7530e11c67 100755 --- a/tests/functional/derivation-advanced-attributes.sh +++ b/tests/functional/derivation-advanced-attributes.sh @@ -12,11 +12,19 @@ badExitCode=0 store="$TEST_ROOT/store" +if [[ -z "${NIX_TESTS_CA_BY_DEFAULT:-}" ]]; then + drvDir=ia + flags=(--arg contentAddress false) +else + drvDir=ca + flags=(--arg contentAddress true --extra-experimental-features ca-derivations) +fi + for nixFile in derivation/*.nix; do - drvPath=$(nix-instantiate --store "$store" --pure-eval --expr "$(< "$nixFile")") + drvPath=$(env -u NIX_STORE nix-instantiate --store "$store" --pure-eval "${flags[@]}" --expr "$(< "$nixFile")") testName=$(basename "$nixFile" .nix) got="${store}${drvPath}" - expected="derivation/$testName.drv" + expected="derivation/${drvDir}/${testName}.drv" diffAndAcceptInner "$testName" "$got" "$expected" done diff --git a/tests/functional/derivation/advanced-attributes-defaults.nix b/tests/functional/derivation/advanced-attributes-defaults.nix index 51a8d0e7e1a..51f359cf042 100644 --- a/tests/functional/derivation/advanced-attributes-defaults.nix +++ b/tests/functional/derivation/advanced-attributes-defaults.nix @@ -1,6 +1,27 @@ -derivation { - name = "advanced-attributes-defaults"; +{ contentAddress }: + +let + caArgs = + if contentAddress then + { + __contentAddressed = true; + outputHashMode = "recursive"; + outputHashAlgo = "sha256"; + } + else + { }; + + derivation' = args: derivation (caArgs // args); + system = "my-system"; + +in +derivation' { + inherit system; + name = "advanced-attributes-defaults"; builder = "/bin/bash"; - args = [ "-c" "echo hello > $out" ]; + args = [ + "-c" + "echo hello > $out" + ]; } diff --git a/tests/functional/derivation/advanced-attributes-structured-attrs-defaults.nix b/tests/functional/derivation/advanced-attributes-structured-attrs-defaults.nix index 0c13a76911f..ec51f0e288f 100644 --- a/tests/functional/derivation/advanced-attributes-structured-attrs-defaults.nix +++ b/tests/functional/derivation/advanced-attributes-structured-attrs-defaults.nix @@ -1,8 +1,32 @@ -derivation { - name = "advanced-attributes-structured-attrs-defaults"; +{ contentAddress }: + +let + caArgs = + if contentAddress then + { + __contentAddressed = true; + outputHashMode = "recursive"; + outputHashAlgo = "sha256"; + } + else + { }; + + derivation' = args: derivation (caArgs // args); + system = "my-system"; + +in +derivation' { + inherit system; + name = "advanced-attributes-structured-attrs-defaults"; builder = "/bin/bash"; - args = [ "-c" "echo hello > $out" ]; - outputs = [ "out" "dev" ]; + args = [ + "-c" + "echo hello > $out" + ]; + outputs = [ + "out" + "dev" + ]; __structuredAttrs = true; } diff --git a/tests/functional/derivation/advanced-attributes-structured-attrs.drv b/tests/functional/derivation/advanced-attributes-structured-attrs.drv deleted file mode 100644 index e47a41ad525..00000000000 --- a/tests/functional/derivation/advanced-attributes-structured-attrs.drv +++ /dev/null @@ -1 +0,0 @@ -Derive([("bin","/nix/store/pbzb48v0ycf80jgligcp4n8z0rblna4n-advanced-attributes-structured-attrs-bin","",""),("dev","/nix/store/7xapi8jv7flcz1qq8jhw55ar8ag8hldh-advanced-attributes-structured-attrs-dev","",""),("out","/nix/store/mpq3l1l1qc2yr50q520g08kprprwv79f-advanced-attributes-structured-attrs","","")],[("/nix/store/4xm4wccqsvagz9gjksn24s7rip2fdy7v-foo.drv",["out"]),("/nix/store/plsq5jbr5nhgqwcgb2qxw7jchc09dnl8-bar.drv",["out"])],[],"my-system","/bin/bash",["-c","echo hello > $out"],[("__json","{\"__darwinAllowLocalNetworking\":true,\"__impureHostDeps\":[\"/usr/bin/ditto\"],\"__noChroot\":true,\"__sandboxProfile\":\"sandcastle\",\"allowSubstitutes\":false,\"builder\":\"/bin/bash\",\"impureEnvVars\":[\"UNICORN\"],\"name\":\"advanced-attributes-structured-attrs\",\"outputChecks\":{\"bin\":{\"disallowedReferences\":[\"/nix/store/7rhsm8i393hm1wcsmph782awg1hi2f7x-bar\"],\"disallowedRequisites\":[\"/nix/store/7rhsm8i393hm1wcsmph782awg1hi2f7x-bar\"]},\"dev\":{\"maxClosureSize\":5909,\"maxSize\":789},\"out\":{\"allowedReferences\":[\"/nix/store/3c08bzb71z4wiag719ipjxr277653ynp-foo\"],\"allowedRequisites\":[\"/nix/store/3c08bzb71z4wiag719ipjxr277653ynp-foo\"]}},\"outputs\":[\"out\",\"bin\",\"dev\"],\"preferLocalBuild\":true,\"requiredSystemFeatures\":[\"rainbow\",\"uid-range\"],\"system\":\"my-system\"}"),("bin","/nix/store/pbzb48v0ycf80jgligcp4n8z0rblna4n-advanced-attributes-structured-attrs-bin"),("dev","/nix/store/7xapi8jv7flcz1qq8jhw55ar8ag8hldh-advanced-attributes-structured-attrs-dev"),("out","/nix/store/mpq3l1l1qc2yr50q520g08kprprwv79f-advanced-attributes-structured-attrs")]) \ No newline at end of file diff --git a/tests/functional/derivation/advanced-attributes-structured-attrs.nix b/tests/functional/derivation/advanced-attributes-structured-attrs.nix index 0044b65fd41..46f619272ec 100644 --- a/tests/functional/derivation/advanced-attributes-structured-attrs.nix +++ b/tests/functional/derivation/advanced-attributes-structured-attrs.nix @@ -1,45 +1,88 @@ +{ contentAddress }: + let + caArgs = + if contentAddress then + { + __contentAddressed = true; + outputHashMode = "recursive"; + outputHashAlgo = "sha256"; + } + else + { }; + + derivation' = args: derivation (caArgs // args); + system = "my-system"; - foo = derivation { + + foo = derivation' { inherit system; name = "foo"; builder = "/bin/bash"; - args = ["-c" "echo foo > $out"]; + args = [ + "-c" + "echo foo > $out" + ]; + outputs = [ + "out" + "dev" + ]; }; - bar = derivation { + + bar = derivation' { inherit system; name = "bar"; builder = "/bin/bash"; - args = ["-c" "echo bar > $out"]; + args = [ + "-c" + "echo bar > $out" + ]; + outputs = [ + "out" + "dev" + ]; }; + in -derivation { +derivation' { inherit system; name = "advanced-attributes-structured-attrs"; builder = "/bin/bash"; - args = [ "-c" "echo hello > $out" ]; + args = [ + "-c" + "echo hello > $out" + ]; __sandboxProfile = "sandcastle"; __noChroot = true; - __impureHostDeps = ["/usr/bin/ditto"]; - impureEnvVars = ["UNICORN"]; + __impureHostDeps = [ "/usr/bin/ditto" ]; + impureEnvVars = [ "UNICORN" ]; __darwinAllowLocalNetworking = true; - outputs = [ "out" "bin" "dev" ]; + outputs = [ + "out" + "bin" + "dev" + ]; __structuredAttrs = true; outputChecks = { out = { - allowedReferences = [foo]; - allowedRequisites = [foo]; + allowedReferences = [ foo ]; + allowedRequisites = [ foo.dev ]; }; bin = { - disallowedReferences = [bar]; - disallowedRequisites = [bar]; + disallowedReferences = [ bar ]; + disallowedRequisites = [ bar.dev ]; }; dev = { maxSize = 789; maxClosureSize = 5909; }; }; - requiredSystemFeatures = ["rainbow" "uid-range"]; + requiredSystemFeatures = [ + "rainbow" + "uid-range" + ]; preferLocalBuild = true; allowSubstitutes = false; + exportReferencesGraph.refs1 = [ foo ]; + exportReferencesGraph.refs2 = [ bar.drvPath ]; } diff --git a/tests/functional/derivation/advanced-attributes.drv b/tests/functional/derivation/advanced-attributes.drv deleted file mode 100644 index ec3112ab2b1..00000000000 --- a/tests/functional/derivation/advanced-attributes.drv +++ /dev/null @@ -1 +0,0 @@ -Derive([("out","/nix/store/33a6fdmn8q9ih9d7npbnrxn2q56a4l8q-advanced-attributes","","")],[("/nix/store/4xm4wccqsvagz9gjksn24s7rip2fdy7v-foo.drv",["out"]),("/nix/store/plsq5jbr5nhgqwcgb2qxw7jchc09dnl8-bar.drv",["out"])],[],"my-system","/bin/bash",["-c","echo hello > $out"],[("__darwinAllowLocalNetworking","1"),("__impureHostDeps","/usr/bin/ditto"),("__noChroot","1"),("__sandboxProfile","sandcastle"),("allowSubstitutes",""),("allowedReferences","/nix/store/3c08bzb71z4wiag719ipjxr277653ynp-foo"),("allowedRequisites","/nix/store/3c08bzb71z4wiag719ipjxr277653ynp-foo"),("builder","/bin/bash"),("disallowedReferences","/nix/store/7rhsm8i393hm1wcsmph782awg1hi2f7x-bar"),("disallowedRequisites","/nix/store/7rhsm8i393hm1wcsmph782awg1hi2f7x-bar"),("impureEnvVars","UNICORN"),("name","advanced-attributes"),("out","/nix/store/33a6fdmn8q9ih9d7npbnrxn2q56a4l8q-advanced-attributes"),("preferLocalBuild","1"),("requiredSystemFeatures","rainbow uid-range"),("system","my-system")]) \ No newline at end of file diff --git a/tests/functional/derivation/advanced-attributes.nix b/tests/functional/derivation/advanced-attributes.nix index ff680c5677f..dd0c09e22d2 100644 --- a/tests/functional/derivation/advanced-attributes.nix +++ b/tests/functional/derivation/advanced-attributes.nix @@ -1,33 +1,76 @@ +{ contentAddress }: + let + caArgs = + if contentAddress then + { + __contentAddressed = true; + outputHashMode = "recursive"; + outputHashAlgo = "sha256"; + } + else + { }; + + derivation' = args: derivation (caArgs // args); + system = "my-system"; - foo = derivation { + + foo = derivation' { inherit system; name = "foo"; builder = "/bin/bash"; - args = ["-c" "echo foo > $out"]; + args = [ + "-c" + "echo foo > $out" + ]; + outputs = [ + "out" + "dev" + ]; }; - bar = derivation { + + bar = derivation' { inherit system; name = "bar"; builder = "/bin/bash"; - args = ["-c" "echo bar > $out"]; + args = [ + "-c" + "echo bar > $out" + ]; + outputs = [ + "out" + "dev" + ]; }; + in -derivation { +derivation' { inherit system; name = "advanced-attributes"; builder = "/bin/bash"; - args = [ "-c" "echo hello > $out" ]; + args = [ + "-c" + "echo hello > $out" + ]; __sandboxProfile = "sandcastle"; __noChroot = true; - __impureHostDeps = ["/usr/bin/ditto"]; - impureEnvVars = ["UNICORN"]; + __impureHostDeps = [ "/usr/bin/ditto" ]; + impureEnvVars = [ "UNICORN" ]; __darwinAllowLocalNetworking = true; - allowedReferences = [foo]; - allowedRequisites = [foo]; - disallowedReferences = [bar]; - disallowedRequisites = [bar]; - requiredSystemFeatures = ["rainbow" "uid-range"]; + allowedReferences = [ foo ]; + allowedRequisites = [ foo.dev ]; + disallowedReferences = [ bar ]; + disallowedRequisites = [ bar.dev ]; + requiredSystemFeatures = [ + "rainbow" + "uid-range" + ]; preferLocalBuild = true; allowSubstitutes = false; + exportReferencesGraph = [ + "refs1" + foo + "refs2" + bar.drvPath + ]; } diff --git a/tests/functional/derivation/ca/advanced-attributes-defaults.drv b/tests/functional/derivation/ca/advanced-attributes-defaults.drv new file mode 100644 index 00000000000..2c81609639b --- /dev/null +++ b/tests/functional/derivation/ca/advanced-attributes-defaults.drv @@ -0,0 +1 @@ +Derive([("out","","r:sha256","")],[],[],"my-system","/bin/bash",["-c","echo hello > $out"],[("builder","/bin/bash"),("name","advanced-attributes-defaults"),("out","/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9"),("outputHashAlgo","sha256"),("outputHashMode","recursive"),("system","my-system")]) \ No newline at end of file diff --git a/tests/functional/derivation/ca/advanced-attributes-structured-attrs-defaults.drv b/tests/functional/derivation/ca/advanced-attributes-structured-attrs-defaults.drv new file mode 100644 index 00000000000..bf56e05d600 --- /dev/null +++ b/tests/functional/derivation/ca/advanced-attributes-structured-attrs-defaults.drv @@ -0,0 +1 @@ +Derive([("dev","","r:sha256",""),("out","","r:sha256","")],[],[],"my-system","/bin/bash",["-c","echo hello > $out"],[("__json","{\"builder\":\"/bin/bash\",\"name\":\"advanced-attributes-structured-attrs-defaults\",\"outputHashAlgo\":\"sha256\",\"outputHashMode\":\"recursive\",\"outputs\":[\"out\",\"dev\"],\"system\":\"my-system\"}"),("dev","/02qcpld1y6xhs5gz9bchpxaw0xdhmsp5dv88lh25r2ss44kh8dxz"),("out","/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9")]) \ No newline at end of file diff --git a/tests/functional/derivation/ca/advanced-attributes-structured-attrs.drv b/tests/functional/derivation/ca/advanced-attributes-structured-attrs.drv new file mode 100644 index 00000000000..cd02c2f8688 --- /dev/null +++ b/tests/functional/derivation/ca/advanced-attributes-structured-attrs.drv @@ -0,0 +1 @@ +Derive([("bin","","r:sha256",""),("dev","","r:sha256",""),("out","","r:sha256","")],[("/nix/store/j56sf12rxpcv5swr14vsjn5cwm6bj03h-foo.drv",["dev","out"]),("/nix/store/qnml92yh97a6fbrs2m5qg5cqlc8vni58-bar.drv",["dev","out"])],["/nix/store/qnml92yh97a6fbrs2m5qg5cqlc8vni58-bar.drv"],"my-system","/bin/bash",["-c","echo hello > $out"],[("__json","{\"__darwinAllowLocalNetworking\":true,\"__impureHostDeps\":[\"/usr/bin/ditto\"],\"__noChroot\":true,\"__sandboxProfile\":\"sandcastle\",\"allowSubstitutes\":false,\"builder\":\"/bin/bash\",\"exportReferencesGraph\":{\"refs1\":[\"/164j69y6zir9z0339n8pjigg3rckinlr77bxsavzizdaaljb7nh9\"],\"refs2\":[\"/nix/store/qnml92yh97a6fbrs2m5qg5cqlc8vni58-bar.drv\"]},\"impureEnvVars\":[\"UNICORN\"],\"name\":\"advanced-attributes-structured-attrs\",\"outputChecks\":{\"bin\":{\"disallowedReferences\":[\"/0nyw57wm2iicnm9rglvjmbci3ikmcp823czdqdzdcgsnnwqps71g\"],\"disallowedRequisites\":[\"/07f301yqyz8c6wf6bbbavb2q39j4n8kmcly1s09xadyhgy6x2wr8\"]},\"dev\":{\"maxClosureSize\":5909,\"maxSize\":789},\"out\":{\"allowedReferences\":[\"/164j69y6zir9z0339n8pjigg3rckinlr77bxsavzizdaaljb7nh9\"],\"allowedRequisites\":[\"/0nr45p69vn6izw9446wsh9bng9nndhvn19kpsm4n96a5mycw0s4z\"]}},\"outputHashAlgo\":\"sha256\",\"outputHashMode\":\"recursive\",\"outputs\":[\"out\",\"bin\",\"dev\"],\"preferLocalBuild\":true,\"requiredSystemFeatures\":[\"rainbow\",\"uid-range\"],\"system\":\"my-system\"}"),("bin","/04f3da1kmbr67m3gzxikmsl4vjz5zf777sv6m14ahv22r65aac9m"),("dev","/02qcpld1y6xhs5gz9bchpxaw0xdhmsp5dv88lh25r2ss44kh8dxz"),("out","/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9")]) \ No newline at end of file diff --git a/tests/functional/derivation/ca/advanced-attributes.drv b/tests/functional/derivation/ca/advanced-attributes.drv new file mode 100644 index 00000000000..068cb593e83 --- /dev/null +++ b/tests/functional/derivation/ca/advanced-attributes.drv @@ -0,0 +1 @@ +Derive([("out","","r:sha256","")],[("/nix/store/j56sf12rxpcv5swr14vsjn5cwm6bj03h-foo.drv",["dev","out"]),("/nix/store/qnml92yh97a6fbrs2m5qg5cqlc8vni58-bar.drv",["dev","out"])],["/nix/store/qnml92yh97a6fbrs2m5qg5cqlc8vni58-bar.drv"],"my-system","/bin/bash",["-c","echo hello > $out"],[("__darwinAllowLocalNetworking","1"),("__impureHostDeps","/usr/bin/ditto"),("__noChroot","1"),("__sandboxProfile","sandcastle"),("allowSubstitutes",""),("allowedReferences","/164j69y6zir9z0339n8pjigg3rckinlr77bxsavzizdaaljb7nh9"),("allowedRequisites","/0nr45p69vn6izw9446wsh9bng9nndhvn19kpsm4n96a5mycw0s4z"),("builder","/bin/bash"),("disallowedReferences","/0nyw57wm2iicnm9rglvjmbci3ikmcp823czdqdzdcgsnnwqps71g"),("disallowedRequisites","/07f301yqyz8c6wf6bbbavb2q39j4n8kmcly1s09xadyhgy6x2wr8"),("exportReferencesGraph","refs1 /164j69y6zir9z0339n8pjigg3rckinlr77bxsavzizdaaljb7nh9 refs2 /nix/store/qnml92yh97a6fbrs2m5qg5cqlc8vni58-bar.drv"),("impureEnvVars","UNICORN"),("name","advanced-attributes"),("out","/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9"),("outputHashAlgo","sha256"),("outputHashMode","recursive"),("preferLocalBuild","1"),("requiredSystemFeatures","rainbow uid-range"),("system","my-system")]) \ No newline at end of file diff --git a/tests/functional/derivation/advanced-attributes-defaults.drv b/tests/functional/derivation/ia/advanced-attributes-defaults.drv similarity index 100% rename from tests/functional/derivation/advanced-attributes-defaults.drv rename to tests/functional/derivation/ia/advanced-attributes-defaults.drv diff --git a/tests/functional/derivation/advanced-attributes-structured-attrs-defaults.drv b/tests/functional/derivation/ia/advanced-attributes-structured-attrs-defaults.drv similarity index 100% rename from tests/functional/derivation/advanced-attributes-structured-attrs-defaults.drv rename to tests/functional/derivation/ia/advanced-attributes-structured-attrs-defaults.drv diff --git a/tests/functional/derivation/ia/advanced-attributes-structured-attrs.drv b/tests/functional/derivation/ia/advanced-attributes-structured-attrs.drv new file mode 100644 index 00000000000..1dfcac42dc5 --- /dev/null +++ b/tests/functional/derivation/ia/advanced-attributes-structured-attrs.drv @@ -0,0 +1 @@ +Derive([("bin","/nix/store/33qms3h55wlaspzba3brlzlrm8m2239g-advanced-attributes-structured-attrs-bin","",""),("dev","/nix/store/wyfgwsdi8rs851wmy1xfzdxy7y5vrg5l-advanced-attributes-structured-attrs-dev","",""),("out","/nix/store/7cxy4zx1vqc885r4jl2l64pymqbdmhii-advanced-attributes-structured-attrs","","")],[("/nix/store/afc3vbjbzql750v2lp8gxgaxsajphzih-foo.drv",["dev","out"]),("/nix/store/vj2i49jm2868j2fmqvxm70vlzmzvgv14-bar.drv",["dev","out"])],["/nix/store/vj2i49jm2868j2fmqvxm70vlzmzvgv14-bar.drv"],"my-system","/bin/bash",["-c","echo hello > $out"],[("__json","{\"__darwinAllowLocalNetworking\":true,\"__impureHostDeps\":[\"/usr/bin/ditto\"],\"__noChroot\":true,\"__sandboxProfile\":\"sandcastle\",\"allowSubstitutes\":false,\"builder\":\"/bin/bash\",\"exportReferencesGraph\":{\"refs1\":[\"/nix/store/p0hax2lzvjpfc2gwkk62xdglz0fcqfzn-foo\"],\"refs2\":[\"/nix/store/vj2i49jm2868j2fmqvxm70vlzmzvgv14-bar.drv\"]},\"impureEnvVars\":[\"UNICORN\"],\"name\":\"advanced-attributes-structured-attrs\",\"outputChecks\":{\"bin\":{\"disallowedReferences\":[\"/nix/store/r5cff30838majxk5mp3ip2diffi8vpaj-bar\"],\"disallowedRequisites\":[\"/nix/store/9b61w26b4avv870dw0ymb6rw4r1hzpws-bar-dev\"]},\"dev\":{\"maxClosureSize\":5909,\"maxSize\":789},\"out\":{\"allowedReferences\":[\"/nix/store/p0hax2lzvjpfc2gwkk62xdglz0fcqfzn-foo\"],\"allowedRequisites\":[\"/nix/store/z0rjzy29v9k5qa4nqpykrbzirj7sd43v-foo-dev\"]}},\"outputs\":[\"out\",\"bin\",\"dev\"],\"preferLocalBuild\":true,\"requiredSystemFeatures\":[\"rainbow\",\"uid-range\"],\"system\":\"my-system\"}"),("bin","/nix/store/33qms3h55wlaspzba3brlzlrm8m2239g-advanced-attributes-structured-attrs-bin"),("dev","/nix/store/wyfgwsdi8rs851wmy1xfzdxy7y5vrg5l-advanced-attributes-structured-attrs-dev"),("out","/nix/store/7cxy4zx1vqc885r4jl2l64pymqbdmhii-advanced-attributes-structured-attrs")]) \ No newline at end of file diff --git a/tests/functional/derivation/ia/advanced-attributes.drv b/tests/functional/derivation/ia/advanced-attributes.drv new file mode 100644 index 00000000000..c71a8888614 --- /dev/null +++ b/tests/functional/derivation/ia/advanced-attributes.drv @@ -0,0 +1 @@ +Derive([("out","/nix/store/wyhpwd748pns4k7svh48wdrc8kvjk0ra-advanced-attributes","","")],[("/nix/store/afc3vbjbzql750v2lp8gxgaxsajphzih-foo.drv",["dev","out"]),("/nix/store/vj2i49jm2868j2fmqvxm70vlzmzvgv14-bar.drv",["dev","out"])],["/nix/store/vj2i49jm2868j2fmqvxm70vlzmzvgv14-bar.drv"],"my-system","/bin/bash",["-c","echo hello > $out"],[("__darwinAllowLocalNetworking","1"),("__impureHostDeps","/usr/bin/ditto"),("__noChroot","1"),("__sandboxProfile","sandcastle"),("allowSubstitutes",""),("allowedReferences","/nix/store/p0hax2lzvjpfc2gwkk62xdglz0fcqfzn-foo"),("allowedRequisites","/nix/store/z0rjzy29v9k5qa4nqpykrbzirj7sd43v-foo-dev"),("builder","/bin/bash"),("disallowedReferences","/nix/store/r5cff30838majxk5mp3ip2diffi8vpaj-bar"),("disallowedRequisites","/nix/store/9b61w26b4avv870dw0ymb6rw4r1hzpws-bar-dev"),("exportReferencesGraph","refs1 /nix/store/p0hax2lzvjpfc2gwkk62xdglz0fcqfzn-foo refs2 /nix/store/vj2i49jm2868j2fmqvxm70vlzmzvgv14-bar.drv"),("impureEnvVars","UNICORN"),("name","advanced-attributes"),("out","/nix/store/wyhpwd748pns4k7svh48wdrc8kvjk0ra-advanced-attributes"),("preferLocalBuild","1"),("requiredSystemFeatures","rainbow uid-range"),("system","my-system")]) \ No newline at end of file diff --git a/tests/functional/dyn-drv/build-built-drv.sh b/tests/functional/dyn-drv/build-built-drv.sh index 647be945716..49d61c6ce26 100644 --- a/tests/functional/dyn-drv/build-built-drv.sh +++ b/tests/functional/dyn-drv/build-built-drv.sh @@ -18,4 +18,9 @@ clearStore drvDep=$(nix-instantiate ./text-hashed-output.nix -A producingDrv) -expectStderr 1 nix build "${drvDep}^out^out" --no-link | grepQuiet "Building dynamic derivations in one shot is not yet implemented" +# Store layer needs bugfix +requireDaemonNewerThan "2.30pre20250515" + +out2=$(nix build "${drvDep}^out^out" --no-link) + +test $out1 == $out2 diff --git a/tests/functional/dyn-drv/dep-built-drv-2.sh b/tests/functional/dyn-drv/dep-built-drv-2.sh new file mode 100644 index 00000000000..0e4cc7c122d --- /dev/null +++ b/tests/functional/dyn-drv/dep-built-drv-2.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +source common.sh + +# Store layer needs bugfix +requireDaemonNewerThan "2.30pre20250515" + +TODO_NixOS # can't enable a sandbox feature easily + +enableFeatures 'recursive-nix' +restartDaemon + +NIX_BIN_DIR="$(dirname "$(type -p nix)")" +export NIX_BIN_DIR + +nix build -L --file ./non-trivial.nix --no-link diff --git a/tests/functional/dyn-drv/dep-built-drv.sh b/tests/functional/dyn-drv/dep-built-drv.sh index 4f6e9b080fa..e9a8b6b832c 100644 --- a/tests/functional/dyn-drv/dep-built-drv.sh +++ b/tests/functional/dyn-drv/dep-built-drv.sh @@ -4,8 +4,11 @@ source common.sh out1=$(nix-build ./text-hashed-output.nix -A hello --no-out-link) +# Store layer needs bugfix +requireDaemonNewerThan "2.30pre20250515" + clearStore -expectStderr 1 nix-build ./text-hashed-output.nix -A wrapper --no-out-link | grepQuiet "Building dynamic derivations in one shot is not yet implemented" +out2=$(nix-build ./text-hashed-output.nix -A wrapper --no-out-link) -# diff -r $out1 $out2 +diff -r $out1 $out2 diff --git a/tests/functional/dyn-drv/failing-outer.sh b/tests/functional/dyn-drv/failing-outer.sh new file mode 100644 index 00000000000..3feda74fbed --- /dev/null +++ b/tests/functional/dyn-drv/failing-outer.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +source common.sh + +# Store layer needs bugfix +requireDaemonNewerThan "2.30pre20250515" + +expected=100 +if [[ -v NIX_DAEMON_PACKAGE ]]; then expected=1; fi # work around the daemon not returning a 100 status correctly + +expectStderr "$expected" nix-build ./text-hashed-output.nix -A failingWrapper --no-out-link \ + | grepQuiet "build of '.*use-dynamic-drv-in-non-dynamic-drv-wrong.drv' failed" diff --git a/tests/functional/dyn-drv/meson.build b/tests/functional/dyn-drv/meson.build index 5b60a46980b..07145000d83 100644 --- a/tests/functional/dyn-drv/meson.build +++ b/tests/functional/dyn-drv/meson.build @@ -12,8 +12,10 @@ suites += { 'recursive-mod-json.sh', 'build-built-drv.sh', 'eval-outputOf.sh', + 'failing-outer.sh', 'dep-built-drv.sh', 'old-daemon-error-hack.sh', + 'dep-built-drv-2.sh', ], 'workdir': meson.current_source_dir(), } diff --git a/tests/functional/dyn-drv/non-trivial.nix b/tests/functional/dyn-drv/non-trivial.nix new file mode 100644 index 00000000000..5cfafbb62f5 --- /dev/null +++ b/tests/functional/dyn-drv/non-trivial.nix @@ -0,0 +1,77 @@ +with import ./config.nix; + +builtins.outputOf + (mkDerivation { + name = "make-derivations.drv"; + + requiredSystemFeatures = [ "recursive-nix" ]; + + buildCommand = '' + set -e + set -u + + PATH=${builtins.getEnv "NIX_BIN_DIR"}:$PATH + + export NIX_CONFIG='extra-experimental-features = nix-command ca-derivations dynamic-derivations' + + declare -A deps=( + [a]="" + [b]="a" + [c]="a" + [d]="b c" + [e]="b c d" + ) + + # Cannot just literally include this, or Nix will think it is the + # *outer* derivation that's trying to refer to itself, and + # substitute the string too soon. + placeholder=$(nix eval --raw --expr 'builtins.placeholder "out"') + + declare -A drvs=() + for word in a b c d e; do + inputDrvs="" + for dep in ''${deps[$word]}; do + if [[ "$inputDrvs" != "" ]]; then + inputDrvs+="," + fi + read -r -d "" line <> \"\$out\""], + "builder": "${shell}", + "env": { + "out": "$placeholder", + "$word": "hello, from $word!", + "PATH": ${builtins.toJSON path} + }, + "inputDrvs": { + $inputDrvs + }, + "inputSrcs": [], + "name": "build-$word", + "outputs": { + "out": { + "method": "nar", + "hashAlgo": "sha256" + } + }, + "system": "${system}" + } + EOF + drvs[$word]="$(echo "$json" | nix derivation add)" + done + cp "''${drvs[e]}" $out + ''; + + __contentAddressed = true; + outputHashMode = "text"; + outputHashAlgo = "sha256"; + }).outPath + "out" diff --git a/tests/functional/dyn-drv/old-daemon-error-hack.nix b/tests/functional/dyn-drv/old-daemon-error-hack.nix index c9d4a62d4f4..d5da3b3ab80 100644 --- a/tests/functional/dyn-drv/old-daemon-error-hack.nix +++ b/tests/functional/dyn-drv/old-daemon-error-hack.nix @@ -1,6 +1,6 @@ with import ./config.nix; -# A simple content-addressed derivation. +# A simple content-addressing derivation. # The derivation can be arbitrarily modified by passing a different `seed`, # but the output will always be the same rec { diff --git a/tests/functional/dyn-drv/recursive-mod-json.nix b/tests/functional/dyn-drv/recursive-mod-json.nix index c6a24ca4f3b..2d46e4e2e02 100644 --- a/tests/functional/dyn-drv/recursive-mod-json.nix +++ b/tests/functional/dyn-drv/recursive-mod-json.nix @@ -1,6 +1,8 @@ with import ./config.nix; -let innerName = "foo"; in +let + innerName = "foo"; +in mkDerivation rec { name = "${innerName}.drv"; diff --git a/tests/functional/dyn-drv/text-hashed-output.nix b/tests/functional/dyn-drv/text-hashed-output.nix index 99203b51849..59261bbbf53 100644 --- a/tests/functional/dyn-drv/text-hashed-output.nix +++ b/tests/functional/dyn-drv/text-hashed-output.nix @@ -1,6 +1,6 @@ with import ./config.nix; -# A simple content-addressed derivation. +# A simple content-addressing derivation. # The derivation can be arbitrarily modified by passing a different `seed`, # but the output will always be the same rec { @@ -13,6 +13,7 @@ rec { echo "Hello World" > $out/hello ''; }; + producingDrv = mkDerivation { name = "hello.drv"; buildCommand = '' @@ -23,6 +24,7 @@ rec { outputHashMode = "text"; outputHashAlgo = "sha256"; }; + wrapper = mkDerivation { name = "use-dynamic-drv-in-non-dynamic-drv"; buildCommand = '' @@ -30,4 +32,12 @@ rec { cp -r ${builtins.outputOf producingDrv.outPath "out"} $out ''; }; + + failingWrapper = mkDerivation { + name = "use-dynamic-drv-in-non-dynamic-drv-wrong"; + buildCommand = '' + echo "Fail at copying the output of the dynamic derivation" + fail ${builtins.outputOf producingDrv.outPath "out"} $out + ''; + }; } diff --git a/tests/functional/eval.nix b/tests/functional/eval.nix index befbd17a9b4..cabf28c2968 100644 --- a/tests/functional/eval.nix +++ b/tests/functional/eval.nix @@ -1,5 +1,5 @@ { int = 123; - str = "foo"; + str = "foo\nbar"; attr.foo = "bar"; } diff --git a/tests/functional/eval.sh b/tests/functional/eval.sh index 7af49d7fdf3..f876f5ac483 100755 --- a/tests/functional/eval.sh +++ b/tests/functional/eval.sh @@ -16,8 +16,8 @@ EOF nix eval --expr 'assert 1 + 2 == 3; true' [[ $(nix eval int -f "./eval.nix") == 123 ]] -[[ $(nix eval str -f "./eval.nix") == '"foo"' ]] -[[ $(nix eval str --raw -f "./eval.nix") == 'foo' ]] +[[ $(nix eval str -f "./eval.nix") == '"foo\nbar"' ]] +[[ $(nix eval str --raw -f "./eval.nix") == $'foo\nbar' ]] [[ "$(nix eval attr -f "./eval.nix")" == '{ foo = "bar"; }' ]] [[ $(nix eval attr --json -f "./eval.nix") == '{"foo":"bar"}' ]] [[ $(nix eval int -f - < "./eval.nix") == 123 ]] @@ -28,7 +28,8 @@ nix eval --expr 'assert 1 + 2 == 3; true' nix-instantiate --eval -E 'assert 1 + 2 == 3; true' [[ $(nix-instantiate -A int --eval "./eval.nix") == 123 ]] -[[ $(nix-instantiate -A str --eval "./eval.nix") == '"foo"' ]] +[[ $(nix-instantiate -A str --eval "./eval.nix") == '"foo\nbar"' ]] +[[ $(nix-instantiate -A str --raw --eval "./eval.nix") == $'foo\nbar' ]] [[ "$(nix-instantiate -A attr --eval "./eval.nix")" == '{ foo = "bar"; }' ]] [[ $(nix-instantiate -A attr --eval --json "./eval.nix") == '{"foo":"bar"}' ]] [[ $(nix-instantiate -A int --eval - < "./eval.nix") == 123 ]] @@ -38,6 +39,9 @@ nix-instantiate --eval -E 'assert 1 + 2 == 3; true' ln -sfn cycle.nix "$TEST_ROOT/cycle.nix" (! nix eval --file "$TEST_ROOT/cycle.nix") +# --file and --pure-eval don't mix. +expectStderr 1 nix eval --pure-eval --file "$TEST_ROOT/cycle.nix" | grepQuiet "not compatible" + # Check that relative symlinks are resolved correctly. mkdir -p "$TEST_ROOT/xyzzy" "$TEST_ROOT/foo" ln -sfn ../xyzzy "$TEST_ROOT/foo/bar" diff --git a/tests/functional/export-graph.nix b/tests/functional/export-graph.nix index 64fe36bd1ef..5078eec8319 100644 --- a/tests/functional/export-graph.nix +++ b/tests/functional/export-graph.nix @@ -2,28 +2,33 @@ with import ./config.nix; rec { - printRefs = - '' - echo $exportReferencesGraph - while read path; do - read drv - read nrRefs - echo "$path has $nrRefs references" - echo "$path" >> $out - for ((n = 0; n < $nrRefs; n++)); do read ref; echo "ref $ref"; test -e "$ref"; done - done < refs - ''; + printRefs = '' + echo $exportReferencesGraph + while read path; do + read drv + read nrRefs + echo "$path has $nrRefs references" + echo "$path" >> $out + for ((n = 0; n < $nrRefs; n++)); do read ref; echo "ref $ref"; test -e "$ref"; done + done < refs + ''; foo."bar.runtimeGraph" = mkDerivation { name = "dependencies"; builder = builtins.toFile "build-graph-builder" "${printRefs}"; - exportReferencesGraph = ["refs" (import ./dependencies.nix {})]; + exportReferencesGraph = [ + "refs" + (import ./dependencies.nix { }) + ]; }; foo."bar.buildGraph" = mkDerivation { name = "dependencies"; builder = builtins.toFile "build-graph-builder" "${printRefs}"; - exportReferencesGraph = ["refs" (import ./dependencies.nix {}).drvPath]; + exportReferencesGraph = [ + "refs" + (import ./dependencies.nix { }).drvPath + ]; }; } diff --git a/tests/functional/failing.nix b/tests/functional/failing.nix index d25e2d6b62b..8abae1856cf 100644 --- a/tests/functional/failing.nix +++ b/tests/functional/failing.nix @@ -2,16 +2,29 @@ with import ./config.nix; let - mkDerivation = args: - derivation ({ - inherit system; - builder = busybox; - args = ["sh" "-e" args.builder or (builtins.toFile "builder-${args.name}.sh" '' - if [ -e "$NIX_ATTRS_SH_FILE" ]; then source $NIX_ATTRS_SH_FILE; fi; - eval "$buildCommand" - '')]; - } // removeAttrs args ["builder" "meta"]) - // { meta = args.meta or {}; }; + mkDerivation = + args: + derivation ( + { + inherit system; + builder = busybox; + args = [ + "sh" + "-e" + args.builder or (builtins.toFile "builder-${args.name}.sh" '' + if [ -e "$NIX_ATTRS_SH_FILE" ]; then source $NIX_ATTRS_SH_FILE; fi; + eval "$buildCommand" + '') + ]; + } + // removeAttrs args [ + "builder" + "meta" + ] + ) + // { + meta = args.meta or { }; + }; in { diff --git a/tests/functional/fetchGit.sh b/tests/functional/fetchGit.sh index 78925b5cdd6..a41aa35c028 100755 --- a/tests/functional/fetchGit.sh +++ b/tests/functional/fetchGit.sh @@ -12,7 +12,7 @@ repo=$TEST_ROOT/./git export _NIX_FORCE_HTTP=1 -rm -rf $repo ${repo}-tmp $TEST_HOME/.cache/nix $TEST_ROOT/worktree $TEST_ROOT/shallow $TEST_ROOT/minimal +rm -rf $repo ${repo}-tmp $TEST_HOME/.cache/nix $TEST_ROOT/worktree $TEST_ROOT/minimal git init $repo git -C $repo config user.email "foobar@example.com" @@ -37,6 +37,7 @@ nix-instantiate --eval -E "builtins.readFile ((builtins.fetchGit file://$TEST_RO # Fetch a worktree. unset _NIX_FORCE_HTTP +expectStderr 0 nix eval -vvvv --impure --raw --expr "(builtins.fetchGit file://$TEST_ROOT/worktree).outPath" | grepQuiet "copying '$TEST_ROOT/worktree/' to the store" path0=$(nix eval --impure --raw --expr "(builtins.fetchGit file://$TEST_ROOT/worktree).outPath") path0_=$(nix eval --impure --raw --expr "(builtins.fetchTree { type = \"git\"; url = file://$TEST_ROOT/worktree; }).outPath") [[ $path0 = $path0_ ]] @@ -64,7 +65,7 @@ git -C $repo add differentbranch git -C $repo commit -m 'Test2' git -C $repo checkout master devrev=$(git -C $repo rev-parse devtest) -nix eval --impure --raw --expr "builtins.fetchGit { url = file://$repo; rev = \"$devrev\"; }" +nix eval --raw --expr "builtins.fetchGit { url = file://$repo; rev = \"$devrev\"; }" [[ $(nix eval --raw --expr "builtins.readFile (builtins.fetchGit { url = file://$repo; rev = \"$devrev\"; allRefs = true; } + \"/differentbranch\")") = 'different file' ]] @@ -79,8 +80,8 @@ path2=$(nix eval --raw --expr "(builtins.fetchGit { url = file://$repo; rev = \" # In pure eval mode, fetchGit with a revision should succeed. [[ $(nix eval --raw --expr "builtins.readFile (fetchGit { url = file://$repo; rev = \"$rev2\"; } + \"/hello\")") = world ]] -# But without a hash, it fails -expectStderr 1 nix eval --expr 'builtins.fetchGit "file:///foo"' | grepQuiet "'fetchGit' will not fetch unlocked input" +# But without a hash, it fails. +expectStderr 1 nix eval --expr 'builtins.fetchGit "file:///foo"' | grepQuiet "'fetchGit' doesn't fetch unlocked input" # Fetch again. This should be cached. mv $repo ${repo}-tmp @@ -141,13 +142,17 @@ path4=$(nix eval --impure --refresh --raw --expr "(builtins.fetchGit file://$rep [[ $(nix eval --impure --expr "builtins.hasAttr \"dirtyRev\" (builtins.fetchGit $repo)") == "false" ]] [[ $(nix eval --impure --expr "builtins.hasAttr \"dirtyShortRev\" (builtins.fetchGit $repo)") == "false" ]] -status=0 -nix eval --impure --raw --expr "(builtins.fetchGit { url = $repo; rev = \"$rev2\"; narHash = \"sha256-B5yIPHhEm0eysJKEsO7nqxprh9vcblFxpJG11gXJus1=\"; }).outPath" || status=$? -[[ "$status" = "102" ]] +expect 102 nix eval --raw --expr "(builtins.fetchGit { url = $repo; rev = \"$rev2\"; narHash = \"sha256-B5yIPHhEm0eysJKEsO7nqxprh9vcblFxpJG11gXJus1=\"; }).outPath" -path5=$(nix eval --impure --raw --expr "(builtins.fetchGit { url = $repo; rev = \"$rev2\"; narHash = \"sha256-Hr8g6AqANb3xqX28eu1XnjK/3ab8Gv6TJSnkb1LezG9=\"; }).outPath") +path5=$(nix eval --raw --expr "(builtins.fetchGit { url = $repo; rev = \"$rev2\"; narHash = \"sha256-Hr8g6AqANb3xqX28eu1XnjK/3ab8Gv6TJSnkb1LezG9=\"; }).outPath") [[ $path = $path5 ]] +# Ensure that NAR hashes are checked. +expectStderr 102 nix eval --raw --expr "(builtins.fetchGit { url = $repo; rev = \"$rev2\"; narHash = \"sha256-Hr8g6AqANb4xqX28eu1XnjK/3ab8Gv6TJSnkb1LezG9=\"; }).outPath" | grepQuiet "error: NAR hash mismatch" + +# It's allowed to use only a narHash, but you should get a warning. +expectStderr 0 nix eval --raw --expr "(builtins.fetchGit { url = $repo; ref = \"tag2\"; narHash = \"sha256-Hr8g6AqANb3xqX28eu1XnjK/3ab8Gv6TJSnkb1LezG9=\"; }).outPath" | grepQuiet "warning: Input .* is unlocked" + # tarball-ttl should be ignored if we specify a rev echo delft > $repo/hello git -C $repo add hello @@ -211,18 +216,6 @@ git -C $TEST_ROOT/minimal fetch $repo $rev2 git -C $TEST_ROOT/minimal checkout $rev2 [[ $(nix eval --impure --raw --expr "(builtins.fetchGit { url = $TEST_ROOT/minimal; }).rev") = $rev2 ]] -# Fetching a shallow repo shouldn't work by default, because we can't -# return a revCount. -git clone --depth 1 file://$repo $TEST_ROOT/shallow -(! nix eval --impure --raw --expr "(builtins.fetchGit { url = $TEST_ROOT/shallow; ref = \"dev\"; }).outPath") - -# But you can request a shallow clone, which won't return a revCount. -path6=$(nix eval --impure --raw --expr "(builtins.fetchTree { type = \"git\"; url = \"file://$TEST_ROOT/shallow\"; ref = \"dev\"; shallow = true; }).outPath") -[[ $path3 = $path6 ]] -[[ $(nix eval --impure --expr "(builtins.fetchTree { type = \"git\"; url = \"file://$TEST_ROOT/shallow\"; ref = \"dev\"; shallow = true; }).revCount or 123") == 123 ]] - -expectStderr 1 nix eval --expr 'builtins.fetchTree { type = "git"; url = "file:///foo"; }' | grepQuiet "'fetchTree' will not fetch unlocked input" - # Explicit ref = "HEAD" should work, and produce the same outPath as without ref path7=$(nix eval --impure --raw --expr "(builtins.fetchGit { url = \"file://$repo\"; ref = \"HEAD\"; }).outPath") path8=$(nix eval --impure --raw --expr "(builtins.fetchGit { url = \"file://$repo\"; }).outPath") @@ -255,7 +248,7 @@ echo "/exported-wonky export-ignore=wonk" >> $repo/.gitattributes git -C $repo add not-exported-file exported-wonky .gitattributes git -C $repo commit -m 'Bla6' rev5=$(git -C $repo rev-parse HEAD) -path12=$(nix eval --impure --raw --expr "(builtins.fetchGit { url = file://$repo; rev = \"$rev5\"; }).outPath") +path12=$(nix eval --raw --expr "(builtins.fetchGit { url = file://$repo; rev = \"$rev5\"; }).outPath") [[ ! -e $path12/not-exported-file ]] [[ -e $path12/exported-wonky ]] @@ -287,17 +280,20 @@ path11=$(nix eval --impure --raw --expr "(builtins.fetchGit ./.).outPath") empty="$TEST_ROOT/empty" git init "$empty" -emptyAttrs='{ lastModified = 0; lastModifiedDate = "19700101000000"; narHash = "sha256-pQpattmS9VmO3ZIQUFn66az8GSmB4IvYhTTCFn6SUmo="; rev = "0000000000000000000000000000000000000000"; revCount = 0; shortRev = "0000000"; submodules = false; }' - -[[ $(nix eval --impure --expr "builtins.removeAttrs (builtins.fetchGit $empty) [\"outPath\"]") = $emptyAttrs ]] +emptyAttrs="{ lastModified = 0; lastModifiedDate = \"19700101000000\"; narHash = \"sha256-pQpattmS9VmO3ZIQUFn66az8GSmB4IvYhTTCFn6SUmo=\"; rev = \"0000000000000000000000000000000000000000\"; revCount = 0; shortRev = \"0000000\"; submodules = false; }" +result=$(nix eval --impure --expr "builtins.removeAttrs (builtins.fetchGit $empty) [\"outPath\"]") +[[ "$result" = "$emptyAttrs" ]] echo foo > "$empty/x" -[[ $(nix eval --impure --expr "builtins.removeAttrs (builtins.fetchGit $empty) [\"outPath\"]") = $emptyAttrs ]] +result=$(nix eval --impure --expr "builtins.removeAttrs (builtins.fetchGit $empty) [\"outPath\"]") +[[ "$result" = "$emptyAttrs" ]] git -C "$empty" add x -[[ $(nix eval --impure --expr "builtins.removeAttrs (builtins.fetchGit $empty) [\"outPath\"]") = '{ lastModified = 0; lastModifiedDate = "19700101000000"; narHash = "sha256-wzlAGjxKxpaWdqVhlq55q5Gxo4Bf860+kLeEa/v02As="; rev = "0000000000000000000000000000000000000000"; revCount = 0; shortRev = "0000000"; submodules = false; }' ]] +expected_attrs="{ lastModified = 0; lastModifiedDate = \"19700101000000\"; narHash = \"sha256-wzlAGjxKxpaWdqVhlq55q5Gxo4Bf860+kLeEa/v02As=\"; rev = \"0000000000000000000000000000000000000000\"; revCount = 0; shortRev = \"0000000\"; submodules = false; }" +result=$(nix eval --impure --expr "builtins.removeAttrs (builtins.fetchGit $empty) [\"outPath\"]") +[[ "$result" = "$expected_attrs" ]] # Test a repo with an empty commit. git -C "$empty" rm -f x diff --git a/tests/functional/fetchGitShallow.sh b/tests/functional/fetchGitShallow.sh new file mode 100644 index 00000000000..4c21bd7ac80 --- /dev/null +++ b/tests/functional/fetchGitShallow.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +# shellcheck source=common.sh +source common.sh + +requireGit + +# Create a test repo with multiple commits for all our tests +git init "$TEST_ROOT/shallow-parent" +git -C "$TEST_ROOT/shallow-parent" config user.email "foobar@example.com" +git -C "$TEST_ROOT/shallow-parent" config user.name "Foobar" + +# Add several commits to have history +echo "{ outputs = _: {}; }" > "$TEST_ROOT/shallow-parent/flake.nix" +echo "" > "$TEST_ROOT/shallow-parent/file.txt" +git -C "$TEST_ROOT/shallow-parent" add file.txt flake.nix +git -C "$TEST_ROOT/shallow-parent" commit -m "First commit" + +echo "second" > "$TEST_ROOT/shallow-parent/file.txt" +git -C "$TEST_ROOT/shallow-parent" commit -m "Second commit" -a + +echo "third" > "$TEST_ROOT/shallow-parent/file.txt" +git -C "$TEST_ROOT/shallow-parent" commit -m "Third commit" -a + +# Add a branch for testing ref fetching +git -C "$TEST_ROOT/shallow-parent" checkout -b dev +echo "branch content" > "$TEST_ROOT/shallow-parent/branch-file.txt" +git -C "$TEST_ROOT/shallow-parent" add branch-file.txt +git -C "$TEST_ROOT/shallow-parent" commit -m "Branch commit" + +# Make a shallow clone (depth=1) +git clone --depth 1 "file://$TEST_ROOT/shallow-parent" "$TEST_ROOT/shallow-clone" + +# Test 1: Fetching a shallow repo shouldn't work by default, because we can't +# return a revCount. +(! nix eval --impure --raw --expr "(builtins.fetchGit { url = \"$TEST_ROOT/shallow-clone\"; ref = \"dev\"; }).outPath") + +# Test 2: But you can request a shallow clone, which won't return a revCount. +path=$(nix eval --impure --raw --expr "(builtins.fetchTree { type = \"git\"; url = \"file://$TEST_ROOT/shallow-clone\"; ref = \"dev\"; shallow = true; }).outPath") +# Verify file from dev branch exists +[[ -f "$path/branch-file.txt" ]] +# Verify revCount is missing +[[ $(nix eval --impure --expr "(builtins.fetchTree { type = \"git\"; url = \"file://$TEST_ROOT/shallow-clone\"; ref = \"dev\"; shallow = true; }).revCount or 123") == 123 ]] + +# Test 3: Check unlocked input error message +expectStderr 1 nix eval --expr 'builtins.fetchTree { type = "git"; url = "file:///foo"; }' | grepQuiet "'fetchTree' doesn't fetch unlocked input" + +# Test 4: Regression test for revCount in worktrees derived from shallow clones +# Add a worktree to the shallow clone +git -C "$TEST_ROOT/shallow-clone" worktree add "$TEST_ROOT/shallow-worktree" + +# Prior to the fix, this would error out because of the shallow clone's +# inability to find parent commits. Now it should return an error. +if nix eval --impure --expr "(builtins.fetchGit { url = \"file://$TEST_ROOT/shallow-worktree\"; }).revCount" 2>/dev/null; then + echo "fetchGit unexpectedly succeeded on shallow clone" >&2 + exit 1 +fi + +# Also verify that fetchTree fails similarly +if nix eval --impure --expr "(builtins.fetchTree { type = \"git\"; url = \"file://$TEST_ROOT/shallow-worktree\"; }).revCount" 2>/dev/null; then + echo "fetchTree unexpectedly succeeded on shallow clone" >&2 + exit 1 +fi + +# Verify that we can shallow fetch the worktree +git -C "$TEST_ROOT/shallow-worktree" rev-list --count HEAD >/dev/null +nix eval --impure --raw --expr "(builtins.fetchGit { url = \"file://$TEST_ROOT/shallow-worktree\"; shallow = true; }).rev" diff --git a/tests/functional/filter-source.nix b/tests/functional/filter-source.nix index 9071636394a..7bad263f842 100644 --- a/tests/functional/filter-source.nix +++ b/tests/functional/filter-source.nix @@ -4,9 +4,12 @@ mkDerivation { name = "filter"; builder = builtins.toFile "builder" "ln -s $input $out"; input = - let filter = path: type: - type != "symlink" - && baseNameOf path != "foo" - && !((import ./lang/lib.nix).hasSuffix ".bak" (baseNameOf path)); - in builtins.filterSource filter ((builtins.getEnv "TEST_ROOT") + "/filterin"); + let + filter = + path: type: + type != "symlink" + && baseNameOf path != "foo" + && !((import ./lang/lib.nix).hasSuffix ".bak" (baseNameOf path)); + in + builtins.filterSource filter ((builtins.getEnv "TEST_ROOT") + "/filterin"); } diff --git a/tests/functional/fixed.nix b/tests/functional/fixed.nix index a920a21671f..eab3ee7073d 100644 --- a/tests/functional/fixed.nix +++ b/tests/functional/fixed.nix @@ -2,15 +2,20 @@ with import ./config.nix; rec { - f2 = dummy: builder: mode: algo: hash: mkDerivation { - name = "fixed"; - inherit builder; - outputHashMode = mode; - outputHashAlgo = algo; - outputHash = hash; - inherit dummy; - impureEnvVars = ["IMPURE_VAR1" "IMPURE_VAR2"]; - }; + f2 = + dummy: builder: mode: algo: hash: + mkDerivation { + name = "fixed"; + inherit builder; + outputHashMode = mode; + outputHashAlgo = algo; + outputHash = hash; + inherit dummy; + impureEnvVars = [ + "IMPURE_VAR1" + "IMPURE_VAR2" + ]; + }; f = f2 ""; @@ -37,7 +42,8 @@ rec { ]; sameAsAdd = - f ./fixed.builder2.sh "recursive" "sha256" "1ixr6yd3297ciyp9im522dfxpqbkhcw0pylkb2aab915278fqaik"; + f ./fixed.builder2.sh "recursive" "sha256" + "1ixr6yd3297ciyp9im522dfxpqbkhcw0pylkb2aab915278fqaik"; bad = [ (f ./fixed.builder1.sh "flat" "md5" "0ddd8be4b179a529afa5f2ffae4b9858") @@ -66,4 +72,7 @@ rec { # Can use "nar" instead of "recursive" now. nar-not-recursive = f2 "foo" ./fixed.builder2.sh "nar" "md5" "3670af73070fa14077ad74e0f5ea4e42"; + + # Experimental feature + git = f2 "foo" ./fixed.builder2.sh "git" "sha1" "cd44baf36915d5dec8374232ea7e2057f3b4494e"; } diff --git a/tests/functional/flakes/common.sh b/tests/functional/flakes/common.sh index b1c3988e342..422cab96cc2 100644 --- a/tests/functional/flakes/common.sh +++ b/tests/functional/flakes/common.sh @@ -88,6 +88,19 @@ writeDependentFlake() { EOF } +writeIfdFlake() { + local flakeDir="$1" + cat > "$flakeDir/flake.nix" < "$flakeDir/flake.nix" <"$flakeDir/flake.nix" <"$flake1Dir/flake.nix" < \$out ''; }; + stack-depth = + let + f = x: if x == 0 then true else f (x - 1); + in + assert (f 100); self.drv; ifd = assert (import self.drv); self.drv; }; } @@ -33,6 +38,12 @@ git -C "$flake1Dir" commit -m "Init" expect 1 nix build "$flake1Dir#foo.bar" 2>&1 | grepQuiet 'error: breaks' expect 1 nix build "$flake1Dir#foo.bar" 2>&1 | grepQuiet 'error: breaks' +# Stack overflow error must not be cached +expect 1 nix build --max-call-depth 50 "$flake1Dir#stack-depth" 2>&1 \ + | grepQuiet 'error: stack overflow; max-call-depth exceeded' +# If the SO is cached, the following invocation will produce a cached failure; we expect it to succeed +nix build --no-link "$flake1Dir#stack-depth" + # Conditional error should not be cached expect 1 nix build "$flake1Dir#ifd" --option allow-import-from-derivation false 2>&1 \ | grepQuiet 'error: cannot build .* during evaluation because the option '\''allow-import-from-derivation'\'' is disabled' diff --git a/tests/functional/flakes/flake-in-submodule.sh b/tests/functional/flakes/flake-in-submodule.sh index 817f77783c0..fe5acf26dec 100755 --- a/tests/functional/flakes/flake-in-submodule.sh +++ b/tests/functional/flakes/flake-in-submodule.sh @@ -27,6 +27,7 @@ git config --global protocol.file.allow always rootRepo=$TEST_ROOT/rootRepo subRepo=$TEST_ROOT/submodule +otherRepo=$TEST_ROOT/otherRepo createGitRepo "$subRepo" @@ -63,3 +64,75 @@ flakeref=git+file://$rootRepo\?submodules=1\&dir=submodule echo '"foo"' > "$rootRepo"/submodule/sub.nix [[ $(nix eval --json "$flakeref#sub" ) = '"foo"' ]] [[ $(nix flake metadata --json "$flakeref" | jq -r .locked.rev) = null ]] + +# Test that `nix flake metadata` parses `submodule` correctly. +cat > "$rootRepo"/flake.nix < "$rootRepo"/flake.nix < "$otherRepo"/flake.nix < "$rootRepo"/flake.nix < "$flake1Dir/foo" git -C "$flake1Dir" add $flake1Dir/foo [[ $(nix flake metadata flake1 --json --refresh | jq -r .dirtyRevision) == "$hash1-dirty" ]] +[[ "$(nix flake metadata flake1 --json | jq -r .fingerprint)" != null ]] echo -n '# foo' >> "$flake1Dir/flake.nix" flake1OriginalCommit=$(git -C "$flake1Dir" rev-parse HEAD) @@ -96,6 +97,9 @@ nix build -o "$TEST_ROOT/result" flake1 nix build -o "$TEST_ROOT/result" "$flake1Dir" nix build -o "$TEST_ROOT/result" "git+file://$flake1Dir" +(cd "$flake1Dir" && nix build -o "$TEST_ROOT/result" ".") +(cd "$flake1Dir" && nix build -o "$TEST_ROOT/result" "path:.") +(cd "$flake1Dir" && nix build -o "$TEST_ROOT/result" "git+file:.") # Test explicit packages.default. nix build -o "$TEST_ROOT/result" "$flake1Dir#default" @@ -105,6 +109,15 @@ nix build -o "$TEST_ROOT/result" "git+file://$flake1Dir#default" nix build -o "$TEST_ROOT/result" "$flake1Dir?ref=HEAD#default" nix build -o "$TEST_ROOT/result" "git+file://$flake1Dir?ref=HEAD#default" +# Check that relative paths are allowed for git flakes. +# This may change in the future once git submodule support is refined. +# See: https://discourse.nixos.org/t/57783 and #9708. +( + # This `cd` should not be required and is indicative of aforementioned bug. + cd "$flake1Dir/.." + nix build -o "$TEST_ROOT/result" "git+file:./$(basename "$flake1Dir")" +) + # Check that store symlinks inside a flake are not interpreted as flakes. nix build -o "$flake1Dir/result" "git+file://$flake1Dir" nix path-info "$flake1Dir/result" @@ -147,6 +160,7 @@ expect 1 nix build -o "$TEST_ROOT/result" "$flake2Dir#bar" --no-update-lock-file nix build -o "$TEST_ROOT/result" "$flake2Dir#bar" --commit-lock-file [[ -e "$flake2Dir/flake.lock" ]] [[ -z $(git -C "$flake2Dir" diff main || echo failed) ]] +[[ $(jq --indent 0 --compact-output . < "$flake2Dir/flake.lock") =~ ^'{"nodes":{"flake1":{"locked":{"lastModified":'.*',"narHash":"sha256-'.*'","ref":"refs/heads/master","rev":"'.*'","revCount":2,"type":"git","url":"file:///'.*'"},"original":{"id":"flake1","type":"indirect"}},"root":{"inputs":{"flake1":"flake1"}}},"root":"root","version":7}'$ ]] # Rerunning the build should not change the lockfile. nix build -o "$TEST_ROOT/result" "$flake2Dir#bar" @@ -207,6 +221,13 @@ nix store gc nix registry list --flake-registry "file://$registry" --refresh | grepQuiet flake3 mv "$registry.tmp" "$registry" +# Ensure that locking ignores the user registry. +mkdir -p "$TEST_HOME/.config/nix" +ln -sfn "$registry" "$TEST_HOME/.config/nix/registry.json" +nix flake metadata --flake-registry '' flake1 +expectStderr 1 nix flake update --flake-registry '' --flake "$flake3Dir" | grepQuiet "cannot find flake 'flake:flake1' in the flake registries" +rm "$TEST_HOME/.config/nix/registry.json" + # Test whether flakes are registered as GC roots for offline use. # FIXME: use tarballs rather than git. rm -rf "$TEST_HOME/.cache" @@ -246,6 +267,7 @@ nix registry add user-flake2 "git+file://$percentEncodedFlake2Dir" [[ $(nix --flake-registry "" registry list | wc -l) == 2 ]] nix --flake-registry "" registry list | grepQuietInverse '^global' # nothing in global registry nix --flake-registry "" registry list | grepQuiet '^user' +nix flake metadata --flake-registry "" user-flake1 | grepQuiet 'URL:.*flake1.*' nix registry remove user-flake1 nix registry remove user-flake2 [[ $(nix registry list | wc -l) == 4 ]] @@ -410,3 +432,41 @@ nix flake metadata "$flake2Dir" --reference-lock-file $TEST_ROOT/flake2-overridd # reference-lock-file can only be used if allow-dirty is set. expectStderr 1 nix flake metadata "$flake2Dir" --no-allow-dirty --reference-lock-file $TEST_ROOT/flake2-overridden.lock + +# After changing an input (flake2 from newFlake2Rev to prevFlake2Rev), we should have the transitive inputs locked by revision $prevFlake2Rev of flake2. +prevFlake1Rev=$(nix flake metadata --json "$flake1Dir" | jq -r .revision) +prevFlake2Rev=$(nix flake metadata --json "$flake2Dir" | jq -r .revision) + +echo "# bla" >> "$flake1Dir/flake.nix" +git -C "$flake1Dir" commit flake.nix -m 'bla' + +nix flake update --flake "$flake2Dir" +git -C "$flake2Dir" commit flake.lock -m 'bla' + +newFlake1Rev=$(nix flake metadata --json "$flake1Dir" | jq -r .revision) +newFlake2Rev=$(nix flake metadata --json "$flake2Dir" | jq -r .revision) + +cat > "$flake3Dir/flake.nix" < "$flake3Dir/flake.nix" < $flakeFollowsA/flake.nix <&1 | grep 'points outside' +expect 1 nix flake lock $flakeFollowsA 2>&1 | grep '/flakeB.*is forbidden in pure evaluation mode' +expect 1 nix flake lock --impure $flakeFollowsA 2>&1 | grep '/flakeB.*does not exist' + +# Test relative non-flake inputs. +cat > $flakeFollowsA/flake.nix < $flakeFollowsA/foo.nix + +git -C $flakeFollowsA add flake.nix foo.nix + +nix flake lock $flakeFollowsA + +[[ $(nix eval --json $flakeFollowsA#e) = 123 ]] # Non-existant follows should print a warning. cat >$flakeFollowsA/flake.nix < $flakeFollowsD/flake.nix +{ outputs = _: {}; } +EOF +cat < $flakeFollowsC/flake.nix +{ + inputs.D.url = "path:nosuchflake"; + outputs = _: {}; +} +EOF +cat < $flakeFollowsB/flake.nix +{ + inputs.C.url = "path:$flakeFollowsC"; + outputs = _: {}; +} +EOF +cat < $flakeFollowsA/flake.nix +{ + inputs.B.url = "path:$flakeFollowsB"; + inputs.D.url = "path:$flakeFollowsD"; + inputs.B.inputs.C.inputs.D.follows = "D"; + outputs = _: {}; +} +EOF + +nix flake lock $flakeFollowsA + +[[ $(jq -c .nodes.C.inputs.D $flakeFollowsA/flake.lock) = '["D"]' ]] + +# Test overlapping flake follows: B has D follow C/D, while A has B/C follow C + +cat < $flakeFollowsC/flake.nix +{ + inputs.D.url = "path:$flakeFollowsD"; + outputs = _: {}; +} +EOF +cat < $flakeFollowsB/flake.nix +{ + inputs.C.url = "path:nosuchflake"; + inputs.D.follows = "C/D"; + outputs = _: {}; +} +EOF +cat < $flakeFollowsA/flake.nix +{ + inputs.B.url = "path:$flakeFollowsB"; + inputs.C.url = "path:$flakeFollowsC"; + inputs.B.inputs.C.follows = "C"; + outputs = _: {}; +} +EOF + +# bug was not triggered without recreating the lockfile +nix flake lock $flakeFollowsA --recreate-lock-file + +[[ $(jq -c .nodes.B.inputs.D $flakeFollowsA/flake.lock) = '["B","C","D"]' ]] + +# Check that you can't have both a flakeref and a follows attribute on an input. +cat < $flakeFollowsB/flake.nix +{ + inputs.C.url = "path:nosuchflake"; + inputs.D.url = "path:nosuchflake"; + inputs.D.follows = "C/D"; + outputs = _: {}; +} +EOF + +expectStderr 1 nix flake lock $flakeFollowsA --recreate-lock-file | grepQuiet "flake input has both a flake reference and a follows attribute" diff --git a/tests/functional/flakes/meson.build b/tests/functional/flakes/meson.build index 00060e3c927..801fefc6f9a 100644 --- a/tests/functional/flakes/meson.build +++ b/tests/functional/flakes/meson.build @@ -27,6 +27,13 @@ suites += { 'shebang.sh', 'commit-lock-file-summary.sh', 'non-flake-inputs.sh', + 'relative-paths.sh', + 'relative-paths-lockfile.sh', + 'symlink-paths.sh', + 'debugger.sh', + 'source-paths.sh', + 'old-lockfiles.sh', + 'trace-ifd.sh', ], 'workdir': meson.current_source_dir(), } diff --git a/tests/functional/flakes/non-flake-inputs.sh b/tests/functional/flakes/non-flake-inputs.sh index f5e12cd0141..05e65604226 100644 --- a/tests/functional/flakes/non-flake-inputs.sh +++ b/tests/functional/flakes/non-flake-inputs.sh @@ -37,11 +37,20 @@ cat > "$flake3Dir/flake.nix" < "$repo/flake.nix" < "$repo/flake.lock" < "$depFlakeA/flake.nix" < "$depFlakeB/flake.nix" < "$subflake/flake.nix" < "$rootFlake/flake.nix" < "$rootFlake/flake.nix" < "$subflake0/flake.nix" < "$subflake1/flake.nix" < "$subflake2/flake.nix" < "$rootFlake/flake.nix" <flake.nix + cat >example/flake.nix <flake.nix <nested-flake1/flake.nix <nested-flake1/nested-flake2/flake.nix < show-output.json +nix eval --impure --expr ' +let show_output = builtins.fromJSON (builtins.readFile ./show-output.json); +in +assert show_output.packages.${builtins.currentSystem}.default == { }; +true +' diff --git a/tests/functional/flakes/source-paths.sh b/tests/functional/flakes/source-paths.sh new file mode 100644 index 00000000000..4709bf2fcec --- /dev/null +++ b/tests/functional/flakes/source-paths.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +source ./common.sh + +requireGit + +repo=$TEST_ROOT/repo + +createGitRepo "$repo" + +cat > "$repo/flake.nix" < "$flakeDir/flake.nix" < "$repoDir/subdir/flake.nix" < "$repoDir/file" + mkdir "$repoDir/subdir" + cat > "$repoDir/subdir/flake.nix" < "$repo2Dir/file" + git -C "$repo2Dir" add flake1_sym file + git -C "$repo2Dir" commit -m Initial + [[ $(nix eval "$repo2Dir/flake1_sym#x") == \"Hello\\n\" ]] + rm -rf "$TEST_ROOT/repo1" "$TEST_ROOT/repo2" +} +test_symlink_from_repo_to_another diff --git a/tests/functional/flakes/trace-ifd.sh b/tests/functional/flakes/trace-ifd.sh new file mode 100644 index 00000000000..4879b97322e --- /dev/null +++ b/tests/functional/flakes/trace-ifd.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +source ./common.sh + +requireGit + +flake1Dir="$TEST_ROOT/flake" + +createGitRepo "$flake1Dir" +createSimpleGitFlake "$flake1Dir" + +cat > "$flake1Dir/flake.nix" <<'EOF' +{ + outputs = { self }: let inherit (import ./config.nix) mkDerivation; in { + drv = mkDerivation { + name = "drv"; + buildCommand = '' + echo drv >$out + ''; + }; + + ifd = mkDerivation { + name = "ifd"; + buildCommand = '' + echo ${builtins.readFile self.drv} >$out + ''; + }; + }; +} +EOF + +nix build --no-link "$flake1Dir#ifd" --option trace-import-from-derivation true 2>&1 \ + | grepQuiet 'warning: built .* during evaluation due to an import from derivation' diff --git a/tests/functional/flakes/unlocked-override.sh b/tests/functional/flakes/unlocked-override.sh index a17a0c2afe7..ed05440de03 100755 --- a/tests/functional/flakes/unlocked-override.sh +++ b/tests/functional/flakes/unlocked-override.sh @@ -30,3 +30,15 @@ git -C "$flake2Dir" add flake.nix echo 456 > "$flake1Dir"/x.nix [[ $(nix eval --json "$flake2Dir#x" --override-input flake1 "$TEST_ROOT/flake1") = 456 ]] + +# Dirty overrides require --allow-dirty-locks. +expectStderr 1 nix flake lock "$flake2Dir" --override-input flake1 "$TEST_ROOT/flake1" | + grepQuiet "Not writing lock file.*because it has an unlocked input" + +nix flake lock "$flake2Dir" --override-input flake1 "$TEST_ROOT/flake1" --allow-dirty-locks + +# Using a lock file with a dirty lock does not require --allow-dirty-locks, but should print a warning. +expectStderr 0 nix eval "$flake2Dir#x" | + grepQuiet "warning: Lock file entry .* is unlocked" + +[[ $(nix eval "$flake2Dir#x") = 456 ]] diff --git a/tests/functional/flamegraph-profiler.sh b/tests/functional/flamegraph-profiler.sh new file mode 100755 index 00000000000..b074507e85f --- /dev/null +++ b/tests/functional/flamegraph-profiler.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash + +source common.sh + +set +x + +expect_trace() { + expr="$1" + expect="$2" + actual=$( + nix-instantiate \ + --eval-profiler flamegraph \ + --eval-profiler-frequency 0 \ + --eval-profile-file /dev/stdout \ + --expr "$expr" | + grep "«string»" || true + ) + + echo -n "Tracing expression '$expr'" + msg=$( + diff -swB \ + <(echo "$expect") \ + <(echo "$actual") + ) && result=0 || result=$? + if [ "$result" -eq 0 ]; then + echo " ok." + else + echo " failed. difference:" + echo "$msg" + return "$result" + fi +} + +# lambda +expect_trace 'let f = arg: arg; in f 1' " +«string»:1:22:f 1 +" + +# unnamed lambda +expect_trace '(arg: arg) 1' " +«string»:1:1 1 +" + +# primop +expect_trace 'builtins.head [0 1]' " +«string»:1:1:primop head 1 +" + +# primop application +expect_trace 'let a = builtins.all (let f = x: x; in f); in a [1]' " +«string»:1:9:primop all 1 +«string»:1:47:primop all 1 +«string»:1:47:primop all;«string»:1:31:f 1 +" + +# functor +expect_trace '{__functor = x: arg: arg;} 1' " +«string»:1:1:functor 1 +«string»:1:1:functor;«string»:1:2 1 +" + +# failure inside a tryEval +expect_trace 'builtins.tryEval (throw "example")' " +«string»:1:1:primop tryEval 1 +«string»:1:1:primop tryEval;«string»:1:19:primop throw 1 +" + +# Missing argument to a formal function +expect_trace 'let f = ({ x }: x); in f { }' " +«string»:1:24:f 1 +" + +# Too many arguments to a formal function +expect_trace 'let f = ({ x }: x); in f { x = "x"; y = "y"; }' " +«string»:1:24:f 1 +" + +# Not enough arguments to a lambda +expect_trace 'let f = (x: y: x + y); in f 1' " +«string»:1:27:f 1 +" + +# Too many arguments to a lambda +expect_trace 'let f2 = (x: x); in f2 1 2' " +«string»:1:21:f2 1 +" + +# Not a function +expect_trace '1 2' " +«string»:1:1 1 +" + +# Derivation +expect_trace 'builtins.derivationStrict { name = "somepackage"; }' " +«string»:1:1:primop derivationStrict:somepackage 1 +" + +# Derivation without name attr +expect_trace 'builtins.derivationStrict { }' " +«string»:1:1:primop derivationStrict 1 +" diff --git a/tests/functional/fmt.sh b/tests/functional/fmt.sh deleted file mode 100755 index e9bff50d5ea..00000000000 --- a/tests/functional/fmt.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash - -source common.sh - -TODO_NixOS # Provide a `shell` variable. Try not to `export` it, perhaps. - -clearStoreIfPossible -rm -rf "$TEST_HOME"/.cache "$TEST_HOME"/.config "$TEST_HOME"/.local - -cp ./simple.nix ./simple.builder.sh ./fmt.simple.sh "${config_nix}" "$TEST_HOME" - -cd "$TEST_HOME" - -nix fmt --help | grep "forward" - -cat << EOF > flake.nix -{ - outputs = _: { - formatter.$system = - with import ./config.nix; - mkDerivation { - name = "formatter"; - buildCommand = '' - mkdir -p \$out/bin - echo "#! ${shell}" > \$out/bin/formatter - cat \${./fmt.simple.sh} >> \$out/bin/formatter - chmod +x \$out/bin/formatter - ''; - }; - }; -} -EOF -# No arguments check -[[ "$(nix fmt)" = "Formatting(0):" ]] -# Argument forwarding check -nix fmt ./file ./folder | grep 'Formatting(2): ./file ./folder' -nix flake check -nix flake show | grep -P "package 'formatter'" diff --git a/tests/functional/fmt.simple.sh b/tests/functional/fmt.simple.sh deleted file mode 100755 index e53f6c9be9a..00000000000 --- a/tests/functional/fmt.simple.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -echo "Formatting(${#}):" "${@}" diff --git a/tests/functional/fod-failing.nix b/tests/functional/fod-failing.nix index 37c04fe12f8..0de676c1536 100644 --- a/tests/functional/fod-failing.nix +++ b/tests/functional/fod-failing.nix @@ -2,38 +2,34 @@ with import ./config.nix; rec { x1 = mkDerivation { name = "x1"; - builder = builtins.toFile "builder.sh" - '' - echo $name > $out - ''; + builder = builtins.toFile "builder.sh" '' + echo $name > $out + ''; outputHashMode = "recursive"; outputHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; }; x2 = mkDerivation { name = "x2"; - builder = builtins.toFile "builder.sh" - '' - echo $name > $out - ''; + builder = builtins.toFile "builder.sh" '' + echo $name > $out + ''; outputHashMode = "recursive"; outputHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; }; x3 = mkDerivation { name = "x3"; - builder = builtins.toFile "builder.sh" - '' - echo $name > $out - ''; + builder = builtins.toFile "builder.sh" '' + echo $name > $out + ''; outputHashMode = "recursive"; outputHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; }; x4 = mkDerivation { name = "x4"; inherit x2 x3; - builder = builtins.toFile "builder.sh" - '' - echo $x2 $x3 - exit 1 - ''; + builder = builtins.toFile "builder.sh" '' + echo $x2 $x3 + exit 1 + ''; }; } diff --git a/tests/functional/formatter.sh b/tests/functional/formatter.sh new file mode 100755 index 00000000000..6631dd6b87a --- /dev/null +++ b/tests/functional/formatter.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash + +source common.sh + +TODO_NixOS # Provide a `shell` variable. Try not to `export` it, perhaps. + +clearStoreIfPossible +rm -rf "$TEST_HOME"/.cache "$TEST_HOME"/.config "$TEST_HOME"/.local + +cp ./simple.nix ./simple.builder.sh ./formatter.simple.sh "${config_nix}" "$TEST_HOME" + +cd "$TEST_HOME" + +nix formatter --help | grep "build or run the formatter" +nix fmt --help | grep "reformat your code" +nix fmt run --help | grep "reformat your code" +nix fmt build --help | grep "build" + +cat << EOF > flake.nix +{ + outputs = _: { + formatter.$system = + with import ./config.nix; + mkDerivation { + name = "formatter"; + buildCommand = '' + mkdir -p \$out/bin + echo "#! ${shell}" > \$out/bin/formatter + cat \${./formatter.simple.sh} >> \$out/bin/formatter + chmod +x \$out/bin/formatter + ''; + }; + }; +} +EOF + +mkdir subflake +cp ./simple.nix ./simple.builder.sh ./formatter.simple.sh "${config_nix}" "$TEST_HOME/subflake" + +cat << EOF > subflake/flake.nix +{ + outputs = _: { + formatter.$system = + with import ./config.nix; + mkDerivation { + name = "formatter"; + buildCommand = '' + mkdir -p \$out/bin + echo "#! ${shell}" > \$out/bin/formatter + cat \${./formatter.simple.sh} >> \$out/bin/formatter + chmod +x \$out/bin/formatter + ''; + }; + }; +} +EOF + +# No arguments check +[[ "$(nix fmt)" = "PRJ_ROOT=$TEST_HOME Formatting(0):" ]] +[[ "$(nix formatter run)" = "PRJ_ROOT=$TEST_HOME Formatting(0):" ]] + +# Argument forwarding check +nix fmt ./file ./folder | grep "PRJ_ROOT=$TEST_HOME Formatting(2): ./file ./folder" +nix formatter run ./file ./folder | grep "PRJ_ROOT=$TEST_HOME Formatting(2): ./file ./folder" + +# test subflake +cd subflake +nix fmt ./file | grep "PRJ_ROOT=$TEST_HOME/subflake Formatting(1): ./file" + +# Build checks +## Defaults to a ./result. +nix formatter build | grep ".\+/bin/formatter" +[[ -L ./result ]] +rm result + +## Can prevent the symlink. +nix formatter build --no-link +[[ ! -e ./result ]] + +## Can change the symlink name. +nix formatter build --out-link my-result | grep ".\+/bin/formatter" +[[ -L ./my-result ]] +rm ./my-result + +# Flake outputs check. +nix flake check +nix flake show | grep -P "package 'formatter'" diff --git a/tests/functional/formatter.simple.sh b/tests/functional/formatter.simple.sh new file mode 100755 index 00000000000..355ff00efbb --- /dev/null +++ b/tests/functional/formatter.simple.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +echo "PRJ_ROOT=$PRJ_ROOT Formatting(${#}):" "${@}" diff --git a/tests/functional/gc-concurrent.nix b/tests/functional/gc-concurrent.nix index 0aba1f98307..d7483d88f12 100644 --- a/tests/functional/gc-concurrent.nix +++ b/tests/functional/gc-concurrent.nix @@ -1,6 +1,8 @@ with import ./config.nix; -{ lockFifo ? null }: +{ + lockFifo ? null, +}: rec { diff --git a/tests/functional/git-hashing/fixed.sh b/tests/functional/git-hashing/fixed.sh new file mode 100755 index 00000000000..f33d95cfa92 --- /dev/null +++ b/tests/functional/git-hashing/fixed.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +source common.sh + +# Store layer needs bugfix +requireDaemonNewerThan "2.27pre20250122" + +nix-build ../fixed.nix -A git --no-out-link diff --git a/tests/functional/git-hashing/meson.build b/tests/functional/git-hashing/meson.build index 470c53fc5bb..d6a782cdccf 100644 --- a/tests/functional/git-hashing/meson.build +++ b/tests/functional/git-hashing/meson.build @@ -3,6 +3,7 @@ suites += { 'deps': [], 'tests': [ 'simple.sh', + 'fixed.sh', ], 'workdir': meson.current_source_dir(), } diff --git a/tests/functional/git-hashing/simple.sh b/tests/functional/git-hashing/simple.sh old mode 100644 new mode 100755 index f43168eb214..e02d8b29761 --- a/tests/functional/git-hashing/simple.sh +++ b/tests/functional/git-hashing/simple.sh @@ -1,3 +1,5 @@ +#!/usr/bin/env bash + source common.sh repo="$TEST_ROOT/scratch" diff --git a/tests/functional/hash-check.nix b/tests/functional/hash-check.nix index 4a8e9b8a8df..7a48a620b79 100644 --- a/tests/functional/hash-check.nix +++ b/tests/functional/hash-check.nix @@ -4,14 +4,22 @@ let { name = "dependencies-input-1"; system = "i086-msdos"; builder = "/bar/sh"; - args = ["-e" "-x" ./dummy]; + args = [ + "-e" + "-x" + ./dummy + ]; }; input2 = derivation { name = "dependencies-input-2"; system = "i086-msdos"; builder = "/bar/sh"; - args = ["-e" "-x" ./dummy]; + args = [ + "-e" + "-x" + ./dummy + ]; outputHashMode = "recursive"; outputHashAlgo = "md5"; outputHash = "ffffffffffffffffffffffffffffffff"; @@ -21,9 +29,13 @@ let { name = "dependencies"; system = "i086-msdos"; builder = "/bar/sh"; - args = ["-e" "-x" (./dummy + "/FOOBAR/../.")]; + args = [ + "-e" + "-x" + (./dummy + "/FOOBAR/../.") + ]; input1 = input1 + "/."; inherit input2; }; -} \ No newline at end of file +} diff --git a/tests/functional/hash-convert.sh b/tests/functional/hash-convert.sh index 3a099950ff9..c40cb469c76 100755 --- a/tests/functional/hash-convert.sh +++ b/tests/functional/hash-convert.sh @@ -93,15 +93,19 @@ try3() { # Asserting input format fails. # - fail=$(nix hash convert --hash-algo "$1" --from nix32 "$2" 2>&1 || echo "exit: $?") - [[ "$fail" == *"error: input hash"*"exit: 1" ]] - fail=$(nix hash convert --hash-algo "$1" --from base16 "$3" 2>&1 || echo "exit: $?") - [[ "$fail" == *"error: input hash"*"exit: 1" ]] - fail=$(nix hash convert --hash-algo "$1" --from nix32 "$4" 2>&1 || echo "exit: $?") - [[ "$fail" == *"error: input hash"*"exit: 1" ]] + expectStderr 1 nix hash convert --hash-algo "$1" --from sri "$2" | grepQuiet "is not SRI" + expectStderr 1 nix hash convert --hash-algo "$1" --from nix32 "$2" | grepQuiet "input hash" + expectStderr 1 nix hash convert --hash-algo "$1" --from base16 "$3" | grepQuiet "input hash" + expectStderr 1 nix hash convert --hash-algo "$1" --from nix32 "$4" | grepQuiet "input hash" + # Base-16 hashes can be in uppercase. + nix hash convert --hash-algo "$1" --from base16 "$(echo $2 | tr [a-z] [A-Z])" } try3 sha1 "800d59cfcd3c05e900cb4e214be48f6b886a08df" "vw46m23bizj4n8afrc0fj19wrp7mj3c0" "gA1Zz808BekAy04hS+SPa4hqCN8=" try3 sha256 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" "1b8m03r63zqhnjf7l5wnldhh7c134ap5vpj0850ymkq1iyzicy5s" "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=" try3 sha512 "204a8fc6dda82f0a0ced7beb8e08a41657c16ef468b228a8279be331a703c33596fd15c13b1b07f9aa1d3bea57789ca031ad85c7a71dd70354ec631238ca3445" "12k9jiq29iyqm03swfsgiw5mlqs173qazm3n7daz43infy12pyrcdf30fkk3qwv4yl2ick8yipc2mqnlh48xsvvxl60lbx8vp38yji0" "IEqPxt2oLwoM7XvrjgikFlfBbvRosiioJ5vjMacDwzWW/RXBOxsH+aodO+pXeJygMa2Fx6cd1wNU7GMSOMo0RQ==" + +# Test SRI hashes that lack trailing '=' characters. These are incorrect but we need to support them for backward compatibility. +[[ $(nix hash convert --from sri "sha256-ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0") = sha256-ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0= ]] +[[ $(nix hash convert --from sri "sha512-IEqPxt2oLwoM7XvrjgikFlfBbvRosiioJ5vjMacDwzWW/RXBOxsH+aodO+pXeJygMa2Fx6cd1wNU7GMSOMo0RQ") = sha512-IEqPxt2oLwoM7XvrjgikFlfBbvRosiioJ5vjMacDwzWW/RXBOxsH+aodO+pXeJygMa2Fx6cd1wNU7GMSOMo0RQ== ]] diff --git a/tests/functional/hash-path.sh b/tests/functional/hash-path.sh index 86d782a9589..4894ae39146 100755 --- a/tests/functional/hash-path.sh +++ b/tests/functional/hash-path.sh @@ -92,3 +92,32 @@ try2 md5 "20f3ffe011d4cfa7d72bfabef7882836" rm "$TEST_ROOT/hash-path/hello" ln -s x "$TEST_ROOT/hash-path/hello" try2 md5 "f78b733a68f5edbdf9413899339eaa4a" + +# Flat mode supports process substitution +h=$(nix hash path --mode flat --type sha256 --base32 <(printf "SMASH THE STATE")) +[[ 0d9n3r2i4m1zgy0wpqbsyabsfzgs952066bfp8gwvcg4mkr4r5g8 == "$h" ]] + +# Flat mode supports process substitution (hash file) +h=$(nix hash file --type sha256 --base32 <(printf "SMASH THE STATE")) +[[ 0d9n3r2i4m1zgy0wpqbsyabsfzgs952066bfp8gwvcg4mkr4r5g8 == "$h" ]] + +# Symlinks in the ancestry are ok and don't affect the result +mkdir -p "$TEST_ROOT/simple" "$TEST_ROOT/try/to/mess/with/it" +echo hi > "$TEST_ROOT/simple/hi" +ln -s "$TEST_ROOT/simple" "$TEST_ROOT/try/to/mess/with/it/simple-link" +h=$(nix hash path --type sha256 --base32 "$TEST_ROOT/simple/hi") +[[ 1xmr8jicvzszfzpz46g37mlpvbzjl2wpwvl2b05psipssyp1sm8h == "$h" ]] +h=$(nix hash path --type sha256 --base32 "$TEST_ROOT/try/to/mess/with/it/simple-link/hi") +[[ 1xmr8jicvzszfzpz46g37mlpvbzjl2wpwvl2b05psipssyp1sm8h == "$h" ]] + +# nix hash --mode nar does not canonicalize a symlink argument. +# Otherwise it can't generate a NAR whose root is a symlink. +# If you want to follow the symlink, pass $(realpath -s ...) instead. +ln -s /non-existent-48cujwe8ndf4as0bne "$TEST_ROOT/symlink-to-nowhere" +h=$(nix hash path --mode nar --type sha256 --base32 "$TEST_ROOT/symlink-to-nowhere") +[[ 1bl5ry3x1fcbwgr5c2x50bn572iixh4j1p6ax5isxly2ddgn8pbp == "$h" ]] # manually verified hash +if [[ -e /bin ]]; then + ln -s /bin "$TEST_ROOT/symlink-to-bin" + h=$(nix hash path --mode nar --type sha256 --base32 "$TEST_ROOT/symlink-to-bin") + [[ 0z2mdmkd43l0ijdxfbj1y8vzli15yh9b09n3a3rrygmjshbyypsw == "$h" ]] # manually verified hash +fi diff --git a/tests/functional/help.sh b/tests/functional/help.sh index 127cc455b62..e1ef75c41f4 100755 --- a/tests/functional/help.sh +++ b/tests/functional/help.sh @@ -2,6 +2,31 @@ source common.sh +function subcommands() { + jq -r ' +def recurse($prefix): + to_entries[] | + ($prefix + [.key]) as $newPrefix | + (if .value | has("commands") then + ($newPrefix, (.value.commands | recurse($newPrefix))) + else + $newPrefix + end); +.args.commands | recurse([]) | join(" ") +' +} + +nix __dump-cli | subcommands | while IFS= read -r cmd; do + # shellcheck disable=SC2086 # word splitting of cmd is intended + nix $cmd --help +done + +[[ $(type -p man) ]] || skipTest "'man' not installed" + +# FIXME: we don't know whether we built the manpages, so we can't +# reliably test them here. +skipTest "we don't know whether we built the manpages, so we can't reliably test them here." + # test help output nix-build --help @@ -49,22 +74,3 @@ nix-daemon --help nix-hash --help nix-instantiate --help nix-prefetch-url --help - -function subcommands() { - jq -r ' -def recurse($prefix): - to_entries[] | - ($prefix + [.key]) as $newPrefix | - (if .value | has("commands") then - ($newPrefix, (.value.commands | recurse($newPrefix))) - else - $newPrefix - end); -.args.commands | recurse([]) | join(" ") -' -} - -nix __dump-cli | subcommands | while IFS= read -r cmd; do - # shellcheck disable=SC2086 # word splitting of cmd is intended - nix $cmd --help -done diff --git a/tests/functional/hermetic.nix b/tests/functional/hermetic.nix index d1dccdff3d5..a5071466474 100644 --- a/tests/functional/hermetic.nix +++ b/tests/functional/hermetic.nix @@ -1,31 +1,51 @@ -{ busybox -, seed -# If we want the final derivation output to have references to its -# dependencies. Some tests need/want this, other don't. -, withFinalRefs ? false +{ + busybox, + seed, + # If we want the final derivation output to have references to its + # dependencies. Some tests need/want this, other don't. + withFinalRefs ? false, }: with import ./config.nix; let contentAddressedByDefault = builtins.getEnv "NIX_TESTS_CA_BY_DEFAULT" == "1"; - caArgs = if contentAddressedByDefault then { - __contentAddressed = true; - outputHashMode = "recursive"; - outputHashAlgo = "sha256"; - } else {}; + caArgs = + if contentAddressedByDefault then + { + __contentAddressed = true; + outputHashMode = "recursive"; + outputHashAlgo = "sha256"; + } + else + { }; - mkDerivation = args: - derivation ({ - inherit system; - builder = busybox; - args = ["sh" "-e" args.builder or (builtins.toFile "builder-${args.name}.sh" '' - if [ -e "$NIX_ATTRS_SH_FILE" ]; then source $NIX_ATTRS_SH_FILE; fi; - eval "$buildCommand" - '')]; - } // removeAttrs args ["builder" "meta" "passthru"] - // caArgs) - // { meta = args.meta or {}; passthru = args.passthru or {}; }; + mkDerivation = + args: + derivation ( + { + inherit system; + builder = busybox; + args = [ + "sh" + "-e" + args.builder or (builtins.toFile "builder-${args.name}.sh" '' + if [ -e "$NIX_ATTRS_SH_FILE" ]; then source $NIX_ATTRS_SH_FILE; fi; + eval "$buildCommand" + '') + ]; + } + // removeAttrs args [ + "builder" + "meta" + "passthru" + ] + // caArgs + ) + // { + meta = args.meta or { }; + passthru = args.passthru or { }; + }; input1 = mkDerivation { shell = busybox; @@ -51,14 +71,15 @@ let in - mkDerivation { - shell = busybox; - name = "hermetic"; - passthru = { inherit input1 input2 input3; }; - buildCommand = - '' - read x < ${input1} - read y < ${input3} - echo ${if (builtins.trace withFinalRefs withFinalRefs) then "${input1} ${input3}" else ""} "$x $y" > $out - ''; - } +mkDerivation { + shell = busybox; + name = "hermetic"; + passthru = { inherit input1 input2 input3; }; + buildCommand = '' + read x < ${input1} + read y < ${input3} + echo ${ + if (builtins.trace withFinalRefs withFinalRefs) then "${input1} ${input3}" else "" + } "$x $y" > $out + ''; +} diff --git a/tests/functional/ifd.nix b/tests/functional/ifd.nix index d0b9b54add0..b8c04f72cac 100644 --- a/tests/functional/ifd.nix +++ b/tests/functional/ifd.nix @@ -1,10 +1,8 @@ with import ./config.nix; -import ( - mkDerivation { - name = "foo"; - bla = import ./dependencies.nix {}; - buildCommand = " +import (mkDerivation { + name = "foo"; + bla = import ./dependencies.nix { }; + buildCommand = " echo \\\"hi\\\" > $out "; - } -) +}) diff --git a/tests/functional/import-from-derivation.nix b/tests/functional/import-from-derivation.nix index cc53451cff3..600f448a6f9 100644 --- a/tests/functional/import-from-derivation.nix +++ b/tests/functional/import-from-derivation.nix @@ -1,12 +1,11 @@ -with import ./config.nix; +with import ; rec { bar = mkDerivation { name = "bar"; - builder = builtins.toFile "builder.sh" - '' - echo 'builtins.add 123 456' > $out - ''; + builder = builtins.toFile "builder.sh" '' + echo 'builtins.add 123 456' > $out + ''; }; value = @@ -16,18 +15,35 @@ rec { result = mkDerivation { name = "foo"; - builder = builtins.toFile "builder.sh" - '' - echo -n FOO${toString value} > $out - ''; + builder = builtins.toFile "builder.sh" '' + echo -n FOO${toString value} > $out + ''; }; addPath = mkDerivation { name = "add-path"; src = builtins.filterSource (path: type: true) result; - builder = builtins.toFile "builder.sh" - '' - echo -n BLA$(cat $src) > $out - ''; + builder = builtins.toFile "builder.sh" '' + echo -n BLA$(cat $src) > $out + ''; }; + + step1 = mkDerivation { + name = "step1"; + buildCommand = '' + mkdir -p $out + echo 'foo' > $out/bla + ''; + }; + + addPathExpr = mkDerivation { + name = "add-path"; + inherit step1; + buildCommand = '' + mkdir -p $out + echo "builtins.path { path = \"$step1\"; sha256 = \"7ptL+pnrZXnSa5hwwB+2SXTLkcSb5264WGGokN8OXto=\"; }" > $out/default.nix + ''; + }; + + importAddPathExpr = import addPathExpr; } diff --git a/tests/functional/import-from-derivation.sh b/tests/functional/import-from-derivation.sh index 83ef92a6f58..a007612350c 100755 --- a/tests/functional/import-from-derivation.sh +++ b/tests/functional/import-from-derivation.sh @@ -6,6 +6,8 @@ TODO_NixOS clearStoreIfPossible +export NIX_PATH=config="${config_nix}" + if nix-instantiate --readonly-mode ./import-from-derivation.nix -A result; then echo "read-only evaluation of an imported derivation unexpectedly failed" exit 1 @@ -15,6 +17,9 @@ outPath=$(nix-build ./import-from-derivation.nix -A result --no-out-link) [ "$(cat "$outPath")" = FOO579 ] +# Check that we can have access to the entire closure of a derivation output. +nix build --no-link --restrict-eval -I src=. -f ./import-from-derivation.nix importAddPathExpr -v + # FIXME: the next tests are broken on CA. if [[ -n "${NIX_TESTS_CA_BY_DEFAULT:-}" ]]; then exit 0 diff --git a/tests/functional/impure-derivations.nix b/tests/functional/impure-derivations.nix index 98547e6c1d6..806f20577d3 100644 --- a/tests/functional/impure-derivations.nix +++ b/tests/functional/impure-derivations.nix @@ -4,60 +4,58 @@ rec { impure = mkDerivation { name = "impure"; - outputs = [ "out" "stuff" ]; - buildCommand = - '' - echo impure - x=$(< $TEST_ROOT/counter) - mkdir $out $stuff - echo $x > $out/n - ln -s $out/n $stuff/bla - printf $((x + 1)) > $TEST_ROOT/counter - ''; + outputs = [ + "out" + "stuff" + ]; + buildCommand = '' + echo impure + x=$(< $TEST_ROOT/counter) + mkdir $out $stuff + echo $x > $out/n + ln -s $out/n $stuff/bla + printf $((x + 1)) > $TEST_ROOT/counter + ''; __impure = true; impureEnvVars = [ "TEST_ROOT" ]; }; impureOnImpure = mkDerivation { name = "impure-on-impure"; - buildCommand = - '' - echo impure-on-impure - x=$(< ${impure}/n) - mkdir $out - printf X$x > $out/n - ln -s ${impure.stuff} $out/symlink - ln -s $out $out/self - ''; + buildCommand = '' + echo impure-on-impure + x=$(< ${impure}/n) + mkdir $out + printf X$x > $out/n + ln -s ${impure.stuff} $out/symlink + ln -s $out $out/self + ''; __impure = true; }; # This is not allowed. inputAddressed = mkDerivation { name = "input-addressed"; - buildCommand = - '' - cat ${impure} > $out - ''; + buildCommand = '' + cat ${impure} > $out + ''; }; contentAddressed = mkDerivation { name = "content-addressed"; - buildCommand = - '' - echo content-addressed - x=$(< ${impureOnImpure}/n) - printf ''${x:0:1} > $out - ''; + buildCommand = '' + echo content-addressed + x=$(< ${impureOnImpure}/n) + printf ''${x:0:1} > $out + ''; outputHashMode = "recursive"; outputHash = "sha256-eBYxcgkuWuiqs4cKNgKwkb3vY/HR0vVsJnqe8itJGcQ="; }; inputAddressedAfterCA = mkDerivation { name = "input-addressed-after-ca"; - buildCommand = - '' - cat ${contentAddressed} > $out - ''; + buildCommand = '' + cat ${contentAddressed} > $out + ''; }; } diff --git a/tests/functional/json.sh b/tests/functional/json.sh new file mode 100644 index 00000000000..49992e0d932 --- /dev/null +++ b/tests/functional/json.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash + +source common.sh + +# Meson would split the output into two buffers, ruining the coherence of the log. +exec 1>&2 + +cat > "$TEST_HOME/expected-machine.json" < "$TEST_HOME/expected-pretty.json" < "$TEST_HOME/actual.json" +diff -U3 "$TEST_HOME/expected-machine.json" "$TEST_HOME/actual.json" + +nix eval --json --pretty --expr \ + '{ a.b.c = true; }' > "$TEST_HOME/actual.json" +diff -U3 "$TEST_HOME/expected-pretty.json" "$TEST_HOME/actual.json" + +if type script &>/dev/null; then + acceptsCommandFlag=0 + # The macOS version just accepts multiple arguments, but util-linux and its `-c` flag only accept a single argument, which is then split on whitespace. We thus have to quote in that case. + if script -c true /dev/null 2>/dev/null; then + acceptsCommandFlag=1 + fi + + runScript() { + if [[ $acceptsCommandFlag -eq 0 ]]; then + script -e -q /dev/null "$@" + else + script -e -q /dev/null -c "$(shellEscapeArray "$@")" + fi + } + runScript nix eval --json --expr "{ a.b.c = true; }" > "$TEST_HOME/actual.json" + cat "$TEST_HOME/actual.json" + # script isn't perfectly accurate? Let's grep for a pretty good indication, as the pretty output has a space between the key and the value. + # diff -U3 "$TEST_HOME/expected-pretty.json" "$TEST_HOME/actual.json" + grep -F '"a": {' "$TEST_HOME/actual.json" + + runScript nix eval --json --pretty --expr "{ a.b.c = true; }" > "$TEST_HOME/actual.json" + cat "$TEST_HOME/actual.json" + grep -F '"a": {' "$TEST_HOME/actual.json" + + runScript nix eval --json --no-pretty --expr "{ a.b.c = true; }" > "$TEST_HOME/actual.json" + cat "$TEST_HOME/actual.json" + grep -F '"a":{' "$TEST_HOME/actual.json" + +fi diff --git a/tests/functional/lang-gc/issue-11141-gc-coroutine-test.nix b/tests/functional/lang-gc/issue-11141-gc-coroutine-test.nix index 4f311af75d7..6dae5c155dd 100644 --- a/tests/functional/lang-gc/issue-11141-gc-coroutine-test.nix +++ b/tests/functional/lang-gc/issue-11141-gc-coroutine-test.nix @@ -1,4 +1,3 @@ - # Run: # GC_INITIAL_HEAP_SIZE=$[1024 * 1024] NIX_SHOW_STATS=1 nix eval -f gc-coroutine-test.nix -vvvv @@ -11,55 +10,56 @@ let # Generate a tree of numbers, n deep, such that the numbers add up to (1 + salt) * 10^n. # The salting makes the numbers all different, increasing the likelihood of catching # any memory corruptions that might be caused by the GC or otherwise. - garbage = salt: n: - if n == 0 - then [(1 + salt)] - else [ - (garbage (10 * salt + 1) (n - 1)) - (garbage (10 * salt - 1) (n - 1)) - (garbage (10 * salt + 2) (n - 1)) - (garbage (10 * salt - 2) (n - 1)) - (garbage (10 * salt + 3) (n - 1)) - (garbage (10 * salt - 3) (n - 1)) - (garbage (10 * salt + 4) (n - 1)) - (garbage (10 * salt - 4) (n - 1)) - (garbage (10 * salt + 5) (n - 1)) - (garbage (10 * salt - 5) (n - 1)) - ]; + garbage = + salt: n: + if n == 0 then + [ (1 + salt) ] + else + [ + (garbage (10 * salt + 1) (n - 1)) + (garbage (10 * salt - 1) (n - 1)) + (garbage (10 * salt + 2) (n - 1)) + (garbage (10 * salt - 2) (n - 1)) + (garbage (10 * salt + 3) (n - 1)) + (garbage (10 * salt - 3) (n - 1)) + (garbage (10 * salt + 4) (n - 1)) + (garbage (10 * salt - 4) (n - 1)) + (garbage (10 * salt + 5) (n - 1)) + (garbage (10 * salt - 5) (n - 1)) + ]; - pow = base: n: - if n == 0 - then 1 - else base * (pow base (n - 1)); + pow = base: n: if n == 0 then 1 else base * (pow base (n - 1)); - sumNestedLists = l: - if isList l - then foldl' (a: b: a + sumNestedLists b) 0 l - else l; + sumNestedLists = l: if isList l then foldl' (a: b: a + sumNestedLists b) 0 l else l; in - assert sumNestedLists (garbage 0 3) == pow 10 3; - assert sumNestedLists (garbage 0 6) == pow 10 6; - builtins.foldl' - (a: b: - assert - "${ - builtins.path { - path = ./src; - filter = path: type: - # We're not doing common subexpression elimination, so this reallocates - # the fairly big tree over and over, producing a lot of garbage during - # source filtering, whose filter runs in a coroutine. - assert sumNestedLists (garbage 0 3) == pow 10 3; - true; - } - }" - == "${./src}"; +assert sumNestedLists (garbage 0 3) == pow 10 3; +assert sumNestedLists (garbage 0 6) == pow 10 6; +builtins.foldl' + ( + a: b: + assert + "${builtins.path { + path = ./src; + filter = + path: type: + # We're not doing common subexpression elimination, so this reallocates + # the fairly big tree over and over, producing a lot of garbage during + # source filtering, whose filter runs in a coroutine. + assert sumNestedLists (garbage 0 3) == pow 10 3; + true; + }}" == "${./src}"; - # These asserts don't seem necessary, as the lambda value get corrupted first - assert a.okay; - assert b.okay; - { okay = true; } - ) + # These asserts don't seem necessary, as the lambda value get corrupted first + assert a.okay; + assert b.okay; + { + okay = true; + } + ) + { okay = true; } + [ + { okay = true; } + { okay = true; } { okay = true; } - [ { okay = true; } { okay = true; } { okay = true; } ] + ] diff --git a/tests/functional/lang/eval-fail-addDrvOutputDependencies-multi-elem-context.err.exp b/tests/functional/lang/eval-fail-addDrvOutputDependencies-multi-elem-context.err.exp index 6828e03c8e7..56fbffa1942 100644 --- a/tests/functional/lang/eval-fail-addDrvOutputDependencies-multi-elem-context.err.exp +++ b/tests/functional/lang/eval-fail-addDrvOutputDependencies-multi-elem-context.err.exp @@ -1,9 +1,9 @@ error: … while calling the 'addDrvOutputDependencies' builtin - at /pwd/lang/eval-fail-addDrvOutputDependencies-multi-elem-context.nix:18:4: - 17| - 18| in builtins.addDrvOutputDependencies combo-path - | ^ - 19| + at /pwd/lang/eval-fail-addDrvOutputDependencies-multi-elem-context.nix:25:1: + 24| in + 25| builtins.addDrvOutputDependencies combo-path + | ^ + 26| error: context of string '/nix/store/pg9yqs4yd85yhdm3f4i5dyaqp5jahrsz-fail.drv/nix/store/2dxd5frb715z451vbf7s8birlf3argbk-fail-2.drv' must have exactly one element, but has 2 diff --git a/tests/functional/lang/eval-fail-addDrvOutputDependencies-multi-elem-context.nix b/tests/functional/lang/eval-fail-addDrvOutputDependencies-multi-elem-context.nix index dbde264dfae..a1c3461cf48 100644 --- a/tests/functional/lang/eval-fail-addDrvOutputDependencies-multi-elem-context.nix +++ b/tests/functional/lang/eval-fail-addDrvOutputDependencies-multi-elem-context.nix @@ -3,16 +3,23 @@ let name = "fail"; builder = "/bin/false"; system = "x86_64-linux"; - outputs = [ "out" "foo" ]; + outputs = [ + "out" + "foo" + ]; }; drv1 = derivation { name = "fail-2"; builder = "/bin/false"; system = "x86_64-linux"; - outputs = [ "out" "foo" ]; + outputs = [ + "out" + "foo" + ]; }; combo-path = "${drv0.drvPath}${drv1.drvPath}"; -in builtins.addDrvOutputDependencies combo-path +in +builtins.addDrvOutputDependencies combo-path diff --git a/tests/functional/lang/eval-fail-addDrvOutputDependencies-wrong-element-kind.err.exp b/tests/functional/lang/eval-fail-addDrvOutputDependencies-wrong-element-kind.err.exp index 72b5e636897..d8399380eb4 100644 --- a/tests/functional/lang/eval-fail-addDrvOutputDependencies-wrong-element-kind.err.exp +++ b/tests/functional/lang/eval-fail-addDrvOutputDependencies-wrong-element-kind.err.exp @@ -1,9 +1,9 @@ error: … while calling the 'addDrvOutputDependencies' builtin - at /pwd/lang/eval-fail-addDrvOutputDependencies-wrong-element-kind.nix:9:4: - 8| - 9| in builtins.addDrvOutputDependencies drv.outPath - | ^ - 10| + at /pwd/lang/eval-fail-addDrvOutputDependencies-wrong-element-kind.nix:13:1: + 12| in + 13| builtins.addDrvOutputDependencies drv.outPath + | ^ + 14| error: `addDrvOutputDependencies` can only act on derivations, not on a derivation output such as 'out' diff --git a/tests/functional/lang/eval-fail-addDrvOutputDependencies-wrong-element-kind.nix b/tests/functional/lang/eval-fail-addDrvOutputDependencies-wrong-element-kind.nix index e379e1d9598..6aab61c4068 100644 --- a/tests/functional/lang/eval-fail-addDrvOutputDependencies-wrong-element-kind.nix +++ b/tests/functional/lang/eval-fail-addDrvOutputDependencies-wrong-element-kind.nix @@ -3,7 +3,11 @@ let name = "fail"; builder = "/bin/false"; system = "x86_64-linux"; - outputs = [ "out" "foo" ]; + outputs = [ + "out" + "foo" + ]; }; -in builtins.addDrvOutputDependencies drv.outPath +in +builtins.addDrvOutputDependencies drv.outPath diff --git a/tests/functional/lang/eval-fail-addErrorContext-example.nix b/tests/functional/lang/eval-fail-addErrorContext-example.nix index 996b2468849..96a9cef84e7 100644 --- a/tests/functional/lang/eval-fail-addErrorContext-example.nix +++ b/tests/functional/lang/eval-fail-addErrorContext-example.nix @@ -1,9 +1,9 @@ let - countDown = n: - if n == 0 - then throw "kaboom" + countDown = + n: + if n == 0 then + throw "kaboom" else - builtins.addErrorContext - "while counting down; n = ${toString n}" - ("x" + countDown (n - 1)); -in countDown 10 + builtins.addErrorContext "while counting down; n = ${toString n}" ("x" + countDown (n - 1)); +in +countDown 10 diff --git a/tests/functional/lang/eval-fail-assert-equal-attrs-names-2.err.exp b/tests/functional/lang/eval-fail-assert-equal-attrs-names-2.err.exp index 4b68d97c20c..5912e6b8c30 100644 --- a/tests/functional/lang/eval-fail-assert-equal-attrs-names-2.err.exp +++ b/tests/functional/lang/eval-fail-assert-equal-attrs-names-2.err.exp @@ -1,8 +1,8 @@ error: … while evaluating the condition of the assertion '({ a = true; } == { a = true; b = true; })' at /pwd/lang/eval-fail-assert-equal-attrs-names-2.nix:1:1: - 1| assert { a = true; } == { a = true; b = true; }; + 1| assert | ^ - 2| throw "unreachable" + 2| { error: attribute names of attribute set '{ a = true; }' differs from attribute set '{ a = true; b = true; }' diff --git a/tests/functional/lang/eval-fail-assert-equal-attrs-names-2.nix b/tests/functional/lang/eval-fail-assert-equal-attrs-names-2.nix index 8e7ac9cf2be..4bce2645612 100644 --- a/tests/functional/lang/eval-fail-assert-equal-attrs-names-2.nix +++ b/tests/functional/lang/eval-fail-assert-equal-attrs-names-2.nix @@ -1,2 +1,8 @@ -assert { a = true; } == { a = true; b = true; }; +assert + { + a = true; + } == { + a = true; + b = true; + }; throw "unreachable" diff --git a/tests/functional/lang/eval-fail-assert-equal-attrs-names.err.exp b/tests/functional/lang/eval-fail-assert-equal-attrs-names.err.exp index bc61ca63a27..a93b26324cc 100644 --- a/tests/functional/lang/eval-fail-assert-equal-attrs-names.err.exp +++ b/tests/functional/lang/eval-fail-assert-equal-attrs-names.err.exp @@ -1,8 +1,8 @@ error: … while evaluating the condition of the assertion '({ a = true; b = true; } == { a = true; })' at /pwd/lang/eval-fail-assert-equal-attrs-names.nix:1:1: - 1| assert { a = true; b = true; } == { a = true; }; + 1| assert | ^ - 2| throw "unreachable" + 2| { error: attribute names of attribute set '{ a = true; b = true; }' differs from attribute set '{ a = true; }' diff --git a/tests/functional/lang/eval-fail-assert-equal-attrs-names.nix b/tests/functional/lang/eval-fail-assert-equal-attrs-names.nix index e2f53a85ad6..f9956999fa4 100644 --- a/tests/functional/lang/eval-fail-assert-equal-attrs-names.nix +++ b/tests/functional/lang/eval-fail-assert-equal-attrs-names.nix @@ -1,2 +1,8 @@ -assert { a = true; b = true; } == { a = true; }; +assert + { + a = true; + b = true; + } == { + a = true; + }; throw "unreachable" diff --git a/tests/functional/lang/eval-fail-assert-equal-derivations-extra.err.exp b/tests/functional/lang/eval-fail-assert-equal-derivations-extra.err.exp index 7f49240747c..9ccf5e4dc10 100644 --- a/tests/functional/lang/eval-fail-assert-equal-derivations-extra.err.exp +++ b/tests/functional/lang/eval-fail-assert-equal-derivations-extra.err.exp @@ -3,23 +3,23 @@ error: at /pwd/lang/eval-fail-assert-equal-derivations-extra.nix:1:1: 1| assert | ^ - 2| { foo = { type = "derivation"; outPath = "/nix/store/0"; }; } + 2| { … while comparing attribute 'foo' … where left hand side is - at /pwd/lang/eval-fail-assert-equal-derivations-extra.nix:2:5: - 1| assert - 2| { foo = { type = "derivation"; outPath = "/nix/store/0"; }; } + at /pwd/lang/eval-fail-assert-equal-derivations-extra.nix:3:5: + 2| { + 3| foo = { | ^ - 3| == + 4| type = "derivation"; … where right hand side is - at /pwd/lang/eval-fail-assert-equal-derivations-extra.nix:4:5: - 3| == - 4| { foo = { type = "derivation"; outPath = "/nix/store/1"; devious = true; }; }; + at /pwd/lang/eval-fail-assert-equal-derivations-extra.nix:8:5: + 7| } == { + 8| foo = { | ^ - 5| throw "unreachable" + 9| type = "derivation"; … while comparing a derivation by its 'outPath' attribute diff --git a/tests/functional/lang/eval-fail-assert-equal-derivations-extra.nix b/tests/functional/lang/eval-fail-assert-equal-derivations-extra.nix index fd8bc3f26ca..14a782a7743 100644 --- a/tests/functional/lang/eval-fail-assert-equal-derivations-extra.nix +++ b/tests/functional/lang/eval-fail-assert-equal-derivations-extra.nix @@ -1,5 +1,14 @@ assert - { foo = { type = "derivation"; outPath = "/nix/store/0"; }; } - == - { foo = { type = "derivation"; outPath = "/nix/store/1"; devious = true; }; }; -throw "unreachable" \ No newline at end of file + { + foo = { + type = "derivation"; + outPath = "/nix/store/0"; + }; + } == { + foo = { + type = "derivation"; + outPath = "/nix/store/1"; + devious = true; + }; + }; +throw "unreachable" diff --git a/tests/functional/lang/eval-fail-assert-equal-derivations.err.exp b/tests/functional/lang/eval-fail-assert-equal-derivations.err.exp index d7f0face077..2be1f48583c 100644 --- a/tests/functional/lang/eval-fail-assert-equal-derivations.err.exp +++ b/tests/functional/lang/eval-fail-assert-equal-derivations.err.exp @@ -3,23 +3,23 @@ error: at /pwd/lang/eval-fail-assert-equal-derivations.nix:1:1: 1| assert | ^ - 2| { foo = { type = "derivation"; outPath = "/nix/store/0"; ignored = abort "not ignored"; }; } + 2| { … while comparing attribute 'foo' … where left hand side is - at /pwd/lang/eval-fail-assert-equal-derivations.nix:2:5: - 1| assert - 2| { foo = { type = "derivation"; outPath = "/nix/store/0"; ignored = abort "not ignored"; }; } + at /pwd/lang/eval-fail-assert-equal-derivations.nix:3:5: + 2| { + 3| foo = { | ^ - 3| == + 4| type = "derivation"; … where right hand side is - at /pwd/lang/eval-fail-assert-equal-derivations.nix:4:5: - 3| == - 4| { foo = { type = "derivation"; outPath = "/nix/store/1"; ignored = abort "not ignored"; }; }; + at /pwd/lang/eval-fail-assert-equal-derivations.nix:9:5: + 8| } == { + 9| foo = { | ^ - 5| throw "unreachable" + 10| type = "derivation"; … while comparing a derivation by its 'outPath' attribute diff --git a/tests/functional/lang/eval-fail-assert-equal-derivations.nix b/tests/functional/lang/eval-fail-assert-equal-derivations.nix index c648eae374b..0f6748c58bf 100644 --- a/tests/functional/lang/eval-fail-assert-equal-derivations.nix +++ b/tests/functional/lang/eval-fail-assert-equal-derivations.nix @@ -1,5 +1,15 @@ assert - { foo = { type = "derivation"; outPath = "/nix/store/0"; ignored = abort "not ignored"; }; } - == - { foo = { type = "derivation"; outPath = "/nix/store/1"; ignored = abort "not ignored"; }; }; -throw "unreachable" \ No newline at end of file + { + foo = { + type = "derivation"; + outPath = "/nix/store/0"; + ignored = abort "not ignored"; + }; + } == { + foo = { + type = "derivation"; + outPath = "/nix/store/1"; + ignored = abort "not ignored"; + }; + }; +throw "unreachable" diff --git a/tests/functional/lang/eval-fail-assert-equal-function-direct.err.exp b/tests/functional/lang/eval-fail-assert-equal-function-direct.err.exp index f06d796981b..93c88a80cd4 100644 --- a/tests/functional/lang/eval-fail-assert-equal-function-direct.err.exp +++ b/tests/functional/lang/eval-fail-assert-equal-function-direct.err.exp @@ -2,8 +2,8 @@ error: … while evaluating the condition of the assertion '((x: x) == (x: x))' at /pwd/lang/eval-fail-assert-equal-function-direct.nix:3:1: 2| # This only compares a direct comparison and makes no claims about functions in nested structures. - 3| assert + 3| assert (x: x) == (x: x); | ^ - 4| (x: x) + 4| abort "unreachable" error: distinct functions and immediate comparisons of identical functions compare as unequal diff --git a/tests/functional/lang/eval-fail-assert-equal-function-direct.nix b/tests/functional/lang/eval-fail-assert-equal-function-direct.nix index 68e5e390823..cd15c4a36d8 100644 --- a/tests/functional/lang/eval-fail-assert-equal-function-direct.nix +++ b/tests/functional/lang/eval-fail-assert-equal-function-direct.nix @@ -1,7 +1,4 @@ # Note: functions in nested structures, e.g. attributes, may be optimized away by pointer identity optimization. # This only compares a direct comparison and makes no claims about functions in nested structures. -assert - (x: x) - == - (x: x); -abort "unreachable" \ No newline at end of file +assert (x: x) == (x: x); +abort "unreachable" diff --git a/tests/functional/lang/eval-fail-assert-equal-list-length.err.exp b/tests/functional/lang/eval-fail-assert-equal-list-length.err.exp index 90108552cf0..e82f3787517 100644 --- a/tests/functional/lang/eval-fail-assert-equal-list-length.err.exp +++ b/tests/functional/lang/eval-fail-assert-equal-list-length.err.exp @@ -1,8 +1,8 @@ error: … while evaluating the condition of the assertion '([ (1) (0) ] == [ (10) ])' at /pwd/lang/eval-fail-assert-equal-list-length.nix:1:1: - 1| assert [ 1 0 ] == [ 10 ]; + 1| assert | ^ - 2| throw "unreachable" + 2| [ error: list of size '2' is not equal to list of size '1', left hand side is '[ 1 0 ]', right hand side is '[ 10 ]' diff --git a/tests/functional/lang/eval-fail-assert-equal-list-length.nix b/tests/functional/lang/eval-fail-assert-equal-list-length.nix index 6d40f4d8e83..bd74ccccd34 100644 --- a/tests/functional/lang/eval-fail-assert-equal-list-length.nix +++ b/tests/functional/lang/eval-fail-assert-equal-list-length.nix @@ -1,2 +1,6 @@ -assert [ 1 0 ] == [ 10 ]; -throw "unreachable" \ No newline at end of file +assert + [ + 1 + 0 + ] == [ 10 ]; +throw "unreachable" diff --git a/tests/functional/lang/eval-fail-assert-equal-paths.nix b/tests/functional/lang/eval-fail-assert-equal-paths.nix index ef0b6702466..647e891b8ac 100644 --- a/tests/functional/lang/eval-fail-assert-equal-paths.nix +++ b/tests/functional/lang/eval-fail-assert-equal-paths.nix @@ -1,2 +1,2 @@ assert ./foo == ./bar; -throw "unreachable" \ No newline at end of file +throw "unreachable" diff --git a/tests/functional/lang/eval-fail-assert-nested-bool.err.exp b/tests/functional/lang/eval-fail-assert-nested-bool.err.exp index 1debb668c98..fdc0818200b 100644 --- a/tests/functional/lang/eval-fail-assert-nested-bool.err.exp +++ b/tests/functional/lang/eval-fail-assert-nested-bool.err.exp @@ -1,74 +1,66 @@ error: … while evaluating the condition of the assertion '({ a = { b = [ ({ c = { d = true; }; }) ]; }; } == { a = { b = [ ({ c = { d = false; }; }) ]; }; })' at /pwd/lang/eval-fail-assert-nested-bool.nix:1:1: - 1| assert + 1| assert { a.b = [ { c.d = true; } ]; } == { a.b = [ { c.d = false; } ]; }; | ^ - 2| { a.b = [ { c.d = true; } ]; } + 2| … while comparing attribute 'a' … where left hand side is - at /pwd/lang/eval-fail-assert-nested-bool.nix:2:5: - 1| assert - 2| { a.b = [ { c.d = true; } ]; } - | ^ - 3| == + at /pwd/lang/eval-fail-assert-nested-bool.nix:1:10: + 1| assert { a.b = [ { c.d = true; } ]; } == { a.b = [ { c.d = false; } ]; }; + | ^ + 2| … where right hand side is - at /pwd/lang/eval-fail-assert-nested-bool.nix:4:5: - 3| == - 4| { a.b = [ { c.d = false; } ]; }; - | ^ - 5| + at /pwd/lang/eval-fail-assert-nested-bool.nix:1:44: + 1| assert { a.b = [ { c.d = true; } ]; } == { a.b = [ { c.d = false; } ]; }; + | ^ + 2| … while comparing attribute 'b' … where left hand side is - at /pwd/lang/eval-fail-assert-nested-bool.nix:2:5: - 1| assert - 2| { a.b = [ { c.d = true; } ]; } - | ^ - 3| == + at /pwd/lang/eval-fail-assert-nested-bool.nix:1:10: + 1| assert { a.b = [ { c.d = true; } ]; } == { a.b = [ { c.d = false; } ]; }; + | ^ + 2| … where right hand side is - at /pwd/lang/eval-fail-assert-nested-bool.nix:4:5: - 3| == - 4| { a.b = [ { c.d = false; } ]; }; - | ^ - 5| + at /pwd/lang/eval-fail-assert-nested-bool.nix:1:44: + 1| assert { a.b = [ { c.d = true; } ]; } == { a.b = [ { c.d = false; } ]; }; + | ^ + 2| … while comparing list element 0 … while comparing attribute 'c' … where left hand side is - at /pwd/lang/eval-fail-assert-nested-bool.nix:2:15: - 1| assert - 2| { a.b = [ { c.d = true; } ]; } - | ^ - 3| == + at /pwd/lang/eval-fail-assert-nested-bool.nix:1:20: + 1| assert { a.b = [ { c.d = true; } ]; } == { a.b = [ { c.d = false; } ]; }; + | ^ + 2| … where right hand side is - at /pwd/lang/eval-fail-assert-nested-bool.nix:4:15: - 3| == - 4| { a.b = [ { c.d = false; } ]; }; - | ^ - 5| + at /pwd/lang/eval-fail-assert-nested-bool.nix:1:54: + 1| assert { a.b = [ { c.d = true; } ]; } == { a.b = [ { c.d = false; } ]; }; + | ^ + 2| … while comparing attribute 'd' … where left hand side is - at /pwd/lang/eval-fail-assert-nested-bool.nix:2:15: - 1| assert - 2| { a.b = [ { c.d = true; } ]; } - | ^ - 3| == + at /pwd/lang/eval-fail-assert-nested-bool.nix:1:20: + 1| assert { a.b = [ { c.d = true; } ]; } == { a.b = [ { c.d = false; } ]; }; + | ^ + 2| … where right hand side is - at /pwd/lang/eval-fail-assert-nested-bool.nix:4:15: - 3| == - 4| { a.b = [ { c.d = false; } ]; }; - | ^ - 5| + at /pwd/lang/eval-fail-assert-nested-bool.nix:1:54: + 1| assert { a.b = [ { c.d = true; } ]; } == { a.b = [ { c.d = false; } ]; }; + | ^ + 2| error: boolean 'true' is not equal to boolean 'false' diff --git a/tests/functional/lang/eval-fail-assert-nested-bool.nix b/tests/functional/lang/eval-fail-assert-nested-bool.nix index 2285769839e..c75fe06106b 100644 --- a/tests/functional/lang/eval-fail-assert-nested-bool.nix +++ b/tests/functional/lang/eval-fail-assert-nested-bool.nix @@ -1,6 +1,3 @@ -assert - { a.b = [ { c.d = true; } ]; } - == - { a.b = [ { c.d = false; } ]; }; +assert { a.b = [ { c.d = true; } ]; } == { a.b = [ { c.d = false; } ]; }; -abort "unreachable" \ No newline at end of file +abort "unreachable" diff --git a/tests/functional/lang/eval-fail-assert.err.exp b/tests/functional/lang/eval-fail-assert.err.exp index 7be9e238797..5fffe79bf0d 100644 --- a/tests/functional/lang/eval-fail-assert.err.exp +++ b/tests/functional/lang/eval-fail-assert.err.exp @@ -1,30 +1,30 @@ error: … while evaluating the attribute 'body' - at /pwd/lang/eval-fail-assert.nix:4:3: - 3| - 4| body = x "x"; + at /pwd/lang/eval-fail-assert.nix:7:3: + 6| + 7| body = x "x"; | ^ - 5| } + 8| } … from call site - at /pwd/lang/eval-fail-assert.nix:4:10: - 3| - 4| body = x "x"; + at /pwd/lang/eval-fail-assert.nix:7:10: + 6| + 7| body = x "x"; | ^ - 5| } + 8| } … while calling 'x' - at /pwd/lang/eval-fail-assert.nix:2:7: - 1| let { - 2| x = arg: assert arg == "y"; 123; - | ^ - 3| + at /pwd/lang/eval-fail-assert.nix:3:5: + 2| x = + 3| arg: + | ^ + 4| assert arg == "y"; … while evaluating the condition of the assertion '(arg == "y")' - at /pwd/lang/eval-fail-assert.nix:2:12: - 1| let { - 2| x = arg: assert arg == "y"; 123; - | ^ - 3| + at /pwd/lang/eval-fail-assert.nix:4:5: + 3| arg: + 4| assert arg == "y"; + | ^ + 5| 123; error: string '"x"' is not equal to string '"y"' diff --git a/tests/functional/lang/eval-fail-assert.nix b/tests/functional/lang/eval-fail-assert.nix index 3b7a1e8bf0c..7cb77504507 100644 --- a/tests/functional/lang/eval-fail-assert.nix +++ b/tests/functional/lang/eval-fail-assert.nix @@ -1,5 +1,8 @@ let { - x = arg: assert arg == "y"; 123; + x = + arg: + assert arg == "y"; + 123; body = x "x"; -} \ No newline at end of file +} diff --git a/tests/functional/lang/eval-fail-attr-name-type.err.exp b/tests/functional/lang/eval-fail-attr-name-type.err.exp index 6848a35ed80..4ea209b130f 100644 --- a/tests/functional/lang/eval-fail-attr-name-type.err.exp +++ b/tests/functional/lang/eval-fail-attr-name-type.err.exp @@ -2,20 +2,20 @@ error: … while evaluating the attribute 'puppy."${key}"' at /pwd/lang/eval-fail-attr-name-type.nix:3:5: 2| attrs = { - 3| puppy.doggy = {}; + 3| puppy.doggy = { }; | ^ 4| }; … while evaluating an attribute name - at /pwd/lang/eval-fail-attr-name-type.nix:7:17: + at /pwd/lang/eval-fail-attr-name-type.nix:7:15: 6| in - 7| attrs.puppy.${key} - | ^ + 7| attrs.puppy.${key} + | ^ 8| error: expected a string but found an integer: 1 - at /pwd/lang/eval-fail-attr-name-type.nix:7:17: + at /pwd/lang/eval-fail-attr-name-type.nix:7:15: 6| in - 7| attrs.puppy.${key} - | ^ + 7| attrs.puppy.${key} + | ^ 8| diff --git a/tests/functional/lang/eval-fail-attr-name-type.nix b/tests/functional/lang/eval-fail-attr-name-type.nix index a0e76004a39..fb6ccdd41d5 100644 --- a/tests/functional/lang/eval-fail-attr-name-type.nix +++ b/tests/functional/lang/eval-fail-attr-name-type.nix @@ -1,7 +1,7 @@ let attrs = { - puppy.doggy = {}; + puppy.doggy = { }; }; key = 1; in - attrs.puppy.${key} +attrs.puppy.${key} diff --git a/tests/functional/lang/eval-fail-attrset-merge-drops-later-rec.err.exp b/tests/functional/lang/eval-fail-attrset-merge-drops-later-rec.err.exp index d1cdc7b769f..ba9185dce1c 100644 --- a/tests/functional/lang/eval-fail-attrset-merge-drops-later-rec.err.exp +++ b/tests/functional/lang/eval-fail-attrset-merge-drops-later-rec.err.exp @@ -1,5 +1,6 @@ error: undefined variable 'd' - at /pwd/lang/eval-fail-attrset-merge-drops-later-rec.nix:1:26: - 1| { a.b = 1; a = rec { c = d + 2; d = 3; }; }.c - | ^ - 2| + at /pwd/lang/eval-fail-attrset-merge-drops-later-rec.nix:4:9: + 3| a = rec { + 4| c = d + 2; + | ^ + 5| d = 3; diff --git a/tests/functional/lang/eval-fail-attrset-merge-drops-later-rec.nix b/tests/functional/lang/eval-fail-attrset-merge-drops-later-rec.nix index fdb314b9193..b6b56bf7d42 100644 --- a/tests/functional/lang/eval-fail-attrset-merge-drops-later-rec.nix +++ b/tests/functional/lang/eval-fail-attrset-merge-drops-later-rec.nix @@ -1 +1,8 @@ -{ a.b = 1; a = rec { c = d + 2; d = 3; }; }.c +{ + a.b = 1; + a = rec { + c = d + 2; + d = 3; + }; +} +.c diff --git a/tests/functional/lang/eval-fail-bad-string-interpolation-4.err.exp b/tests/functional/lang/eval-fail-bad-string-interpolation-4.err.exp index b262e814dbc..ea5910072c3 100644 --- a/tests/functional/lang/eval-fail-bad-string-interpolation-4.err.exp +++ b/tests/functional/lang/eval-fail-bad-string-interpolation-4.err.exp @@ -1,9 +1,9 @@ error: … while evaluating a path segment - at /pwd/lang/eval-fail-bad-string-interpolation-4.nix:9:3: - 8| # The error message should not be too long. - 9| ''${pkgs}'' + at /pwd/lang/eval-fail-bad-string-interpolation-4.nix:19:3: + 18| # The error message should not be too long. + 19| ''${pkgs}'' | ^ - 10| + 20| error: cannot coerce a set to a string: { a = { a = { a = { a = "ha"; b = "ha"; c = "ha"; d = "ha"; e = "ha"; f = "ha"; g = "ha"; h = "ha"; j = "ha"; }; «8 attributes elided» }; «8 attributes elided» }; «8 attributes elided» } diff --git a/tests/functional/lang/eval-fail-bad-string-interpolation-4.nix b/tests/functional/lang/eval-fail-bad-string-interpolation-4.nix index 457b5f06a88..e8349bbdff3 100644 --- a/tests/functional/lang/eval-fail-bad-string-interpolation-4.nix +++ b/tests/functional/lang/eval-fail-bad-string-interpolation-4.nix @@ -1,6 +1,16 @@ let # Basically a "billion laughs" attack, but toned down to simulated `pkgs`. - ha = x: y: { a = x y; b = x y; c = x y; d = x y; e = x y; f = x y; g = x y; h = x y; j = x y; }; + ha = x: y: { + a = x y; + b = x y; + c = x y; + d = x y; + e = x y; + f = x y; + g = x y; + h = x y; + j = x y; + }; has = ha (ha (ha (ha (x: x)))) "ha"; # A large structure that has already been evaluated. pkgs = builtins.deepSeq has has; diff --git a/tests/functional/lang/eval-fail-derivation-name.err.exp b/tests/functional/lang/eval-fail-derivation-name.err.exp index 0ef98674d81..017326c3490 100644 --- a/tests/functional/lang/eval-fail-derivation-name.err.exp +++ b/tests/functional/lang/eval-fail-derivation-name.err.exp @@ -1,17 +1,17 @@ error: … while evaluating the attribute 'outPath' at ::: - | value = commonAttrs // { - | outPath = builtins.getAttr outputName strict; - | ^ - | drvPath = strict.drvPath; + | value = commonAttrs // { + | outPath = builtins.getAttr outputName strict; + | ^ + | drvPath = strict.drvPath; … while calling the 'getAttr' builtin at ::: - | value = commonAttrs // { - | outPath = builtins.getAttr outputName strict; - | ^ - | drvPath = strict.drvPath; + | value = commonAttrs // { + | outPath = builtins.getAttr outputName strict; + | ^ + | drvPath = strict.drvPath; … while calling the 'derivationStrict' builtin at ::: diff --git a/tests/functional/lang/eval-fail-dup-dynamic-attrs.err.exp b/tests/functional/lang/eval-fail-dup-dynamic-attrs.err.exp index 834f9c67bc4..4eafe945b74 100644 --- a/tests/functional/lang/eval-fail-dup-dynamic-attrs.err.exp +++ b/tests/functional/lang/eval-fail-dup-dynamic-attrs.err.exp @@ -2,13 +2,13 @@ error: … while evaluating the attribute 'set' at /pwd/lang/eval-fail-dup-dynamic-attrs.nix:2:3: 1| { - 2| set = { "${"" + "b"}" = 1; }; + 2| set = { | ^ - 3| set = { "${"b" + ""}" = 2; }; + 3| "${"" + "b"}" = 1; - error: dynamic attribute 'b' already defined at /pwd/lang/eval-fail-dup-dynamic-attrs.nix:2:11 - at /pwd/lang/eval-fail-dup-dynamic-attrs.nix:3:11: - 2| set = { "${"" + "b"}" = 1; }; - 3| set = { "${"b" + ""}" = 2; }; - | ^ - 4| } + error: dynamic attribute 'b' already defined at /pwd/lang/eval-fail-dup-dynamic-attrs.nix:3:5 + at /pwd/lang/eval-fail-dup-dynamic-attrs.nix:6:5: + 5| set = { + 6| "${"b" + ""}" = 2; + | ^ + 7| }; diff --git a/tests/functional/lang/eval-fail-dup-dynamic-attrs.nix b/tests/functional/lang/eval-fail-dup-dynamic-attrs.nix index 7ea17f6c878..93cceefa48e 100644 --- a/tests/functional/lang/eval-fail-dup-dynamic-attrs.nix +++ b/tests/functional/lang/eval-fail-dup-dynamic-attrs.nix @@ -1,4 +1,8 @@ { - set = { "${"" + "b"}" = 1; }; - set = { "${"b" + ""}" = 2; }; + set = { + "${"" + "b"}" = 1; + }; + set = { + "${"b" + ""}" = 2; + }; } diff --git a/tests/functional/lang/eval-fail-duplicate-traces.err.exp b/tests/functional/lang/eval-fail-duplicate-traces.err.exp index cedaebd3b58..e6ae60f3ca0 100644 --- a/tests/functional/lang/eval-fail-duplicate-traces.err.exp +++ b/tests/functional/lang/eval-fail-duplicate-traces.err.exp @@ -1,51 +1,51 @@ error: … from call site - at /pwd/lang/eval-fail-duplicate-traces.nix:9:3: - 8| in - 9| throwAfter 2 - | ^ - 10| + at /pwd/lang/eval-fail-duplicate-traces.nix:6:1: + 5| in + 6| throwAfter 2 + | ^ + 7| … while calling 'throwAfter' at /pwd/lang/eval-fail-duplicate-traces.nix:4:16: 3| let - 4| throwAfter = n: + 4| throwAfter = n: if n > 0 then throwAfter (n - 1) else throw "Uh oh!"; | ^ - 5| if n > 0 + 5| in … from call site - at /pwd/lang/eval-fail-duplicate-traces.nix:6:10: - 5| if n > 0 - 6| then throwAfter (n - 1) - | ^ - 7| else throw "Uh oh!"; + at /pwd/lang/eval-fail-duplicate-traces.nix:4:33: + 3| let + 4| throwAfter = n: if n > 0 then throwAfter (n - 1) else throw "Uh oh!"; + | ^ + 5| in … while calling 'throwAfter' at /pwd/lang/eval-fail-duplicate-traces.nix:4:16: 3| let - 4| throwAfter = n: + 4| throwAfter = n: if n > 0 then throwAfter (n - 1) else throw "Uh oh!"; | ^ - 5| if n > 0 + 5| in … from call site - at /pwd/lang/eval-fail-duplicate-traces.nix:6:10: - 5| if n > 0 - 6| then throwAfter (n - 1) - | ^ - 7| else throw "Uh oh!"; + at /pwd/lang/eval-fail-duplicate-traces.nix:4:33: + 3| let + 4| throwAfter = n: if n > 0 then throwAfter (n - 1) else throw "Uh oh!"; + | ^ + 5| in … while calling 'throwAfter' at /pwd/lang/eval-fail-duplicate-traces.nix:4:16: 3| let - 4| throwAfter = n: + 4| throwAfter = n: if n > 0 then throwAfter (n - 1) else throw "Uh oh!"; | ^ - 5| if n > 0 + 5| in … while calling the 'throw' builtin - at /pwd/lang/eval-fail-duplicate-traces.nix:7:10: - 6| then throwAfter (n - 1) - 7| else throw "Uh oh!"; - | ^ - 8| in + at /pwd/lang/eval-fail-duplicate-traces.nix:4:57: + 3| let + 4| throwAfter = n: if n > 0 then throwAfter (n - 1) else throw "Uh oh!"; + | ^ + 5| in error: Uh oh! diff --git a/tests/functional/lang/eval-fail-duplicate-traces.nix b/tests/functional/lang/eval-fail-duplicate-traces.nix index 17ce374ece7..90526f6d48c 100644 --- a/tests/functional/lang/eval-fail-duplicate-traces.nix +++ b/tests/functional/lang/eval-fail-duplicate-traces.nix @@ -1,9 +1,6 @@ # Check that we only omit duplicate stack traces when there's a bunch of them. # Here, there's only a couple duplicate entries, so we output them all. let - throwAfter = n: - if n > 0 - then throwAfter (n - 1) - else throw "Uh oh!"; + throwAfter = n: if n > 0 then throwAfter (n - 1) else throw "Uh oh!"; in - throwAfter 2 +throwAfter 2 diff --git a/tests/functional/lang/eval-fail-fetchTree-negative.err.exp b/tests/functional/lang/eval-fail-fetchTree-negative.err.exp index d9ba1f0b2f8..423123ca0a7 100644 --- a/tests/functional/lang/eval-fail-fetchTree-negative.err.exp +++ b/tests/functional/lang/eval-fail-fetchTree-negative.err.exp @@ -5,4 +5,4 @@ error: | ^ 2| type = "file"; - error: negative value given for fetchTree attr owner: -1 + error: negative value given for 'fetchTree' argument 'owner': -1 diff --git a/tests/functional/lang/eval-fail-fetchurl-baseName-attrs-name.err.exp b/tests/functional/lang/eval-fail-fetchurl-baseName-attrs-name.err.exp index 30f8b6a3544..2cac02f5875 100644 --- a/tests/functional/lang/eval-fail-fetchurl-baseName-attrs-name.err.exp +++ b/tests/functional/lang/eval-fail-fetchurl-baseName-attrs-name.err.exp @@ -1,8 +1,8 @@ error: … while calling the 'fetchurl' builtin at /pwd/lang/eval-fail-fetchurl-baseName-attrs-name.nix:1:1: - 1| builtins.fetchurl { url = "https://example.com/foo.tar.gz"; name = "~wobble~"; } + 1| builtins.fetchurl { | ^ - 2| + 2| url = "https://example.com/foo.tar.gz"; error: invalid store path name when fetching URL 'https://example.com/foo.tar.gz': name '~wobble~' contains illegal character '~'. Please change the value for the 'name' attribute passed to 'fetchurl', so that it can create a valid store path. diff --git a/tests/functional/lang/eval-fail-fetchurl-baseName-attrs-name.nix b/tests/functional/lang/eval-fail-fetchurl-baseName-attrs-name.nix index 5838055390d..dcaf7202b11 100644 --- a/tests/functional/lang/eval-fail-fetchurl-baseName-attrs-name.nix +++ b/tests/functional/lang/eval-fail-fetchurl-baseName-attrs-name.nix @@ -1 +1,4 @@ -builtins.fetchurl { url = "https://example.com/foo.tar.gz"; name = "~wobble~"; } +builtins.fetchurl { + url = "https://example.com/foo.tar.gz"; + name = "~wobble~"; +} diff --git a/tests/functional/lang/eval-fail-flake-ref-to-string-negative-integer.err.exp b/tests/functional/lang/eval-fail-flake-ref-to-string-negative-integer.err.exp index 25c8d7eaaa8..2b56939c621 100644 --- a/tests/functional/lang/eval-fail-flake-ref-to-string-negative-integer.err.exp +++ b/tests/functional/lang/eval-fail-flake-ref-to-string-negative-integer.err.exp @@ -1,14 +1,16 @@ error: … while calling the 'seq' builtin - at /pwd/lang/eval-fail-flake-ref-to-string-negative-integer.nix:1:16: - 1| let n = -1; in builtins.seq n (builtins.flakeRefToString { - | ^ - 2| type = "github"; + at /pwd/lang/eval-fail-flake-ref-to-string-negative-integer.nix:4:1: + 3| in + 4| builtins.seq n ( + | ^ + 5| builtins.flakeRefToString { … while calling the 'flakeRefToString' builtin - at /pwd/lang/eval-fail-flake-ref-to-string-negative-integer.nix:1:32: - 1| let n = -1; in builtins.seq n (builtins.flakeRefToString { - | ^ - 2| type = "github"; + at /pwd/lang/eval-fail-flake-ref-to-string-negative-integer.nix:5:3: + 4| builtins.seq n ( + 5| builtins.flakeRefToString { + | ^ + 6| type = "github"; error: negative value given for flake ref attr repo: -1 diff --git a/tests/functional/lang/eval-fail-flake-ref-to-string-negative-integer.nix b/tests/functional/lang/eval-fail-flake-ref-to-string-negative-integer.nix index e0208eb2519..9cc9ef6295b 100644 --- a/tests/functional/lang/eval-fail-flake-ref-to-string-negative-integer.nix +++ b/tests/functional/lang/eval-fail-flake-ref-to-string-negative-integer.nix @@ -1,7 +1,12 @@ -let n = -1; in builtins.seq n (builtins.flakeRefToString { - type = "github"; - owner = "NixOS"; - repo = n; - ref = "23.05"; - dir = "lib"; -}) +let + n = -1; +in +builtins.seq n ( + builtins.flakeRefToString { + type = "github"; + owner = "NixOS"; + repo = n; + ref = "23.05"; + dir = "lib"; + } +) diff --git a/tests/functional/lang/eval-fail-foldlStrict-strict-op-application.err.exp b/tests/functional/lang/eval-fail-foldlStrict-strict-op-application.err.exp index 4903bc82d54..bb02ecdcb8f 100644 --- a/tests/functional/lang/eval-fail-foldlStrict-strict-op-application.err.exp +++ b/tests/functional/lang/eval-fail-foldlStrict-strict-op-application.err.exp @@ -2,36 +2,36 @@ error: … while calling the 'foldl'' builtin at /pwd/lang/eval-fail-foldlStrict-strict-op-application.nix:2:1: 1| # Tests that the result of applying op is forced even if the value is never used - 2| builtins.foldl' + 2| builtins.foldl' (_: f: f null) null [ | ^ - 3| (_: f: f null) + 3| (_: throw "Not the final value, but is still forced!") … while calling anonymous lambda - at /pwd/lang/eval-fail-foldlStrict-strict-op-application.nix:3:7: - 2| builtins.foldl' - 3| (_: f: f null) - | ^ - 4| null + at /pwd/lang/eval-fail-foldlStrict-strict-op-application.nix:2:21: + 1| # Tests that the result of applying op is forced even if the value is never used + 2| builtins.foldl' (_: f: f null) null [ + | ^ + 3| (_: throw "Not the final value, but is still forced!") … from call site - at /pwd/lang/eval-fail-foldlStrict-strict-op-application.nix:3:10: - 2| builtins.foldl' - 3| (_: f: f null) - | ^ - 4| null + at /pwd/lang/eval-fail-foldlStrict-strict-op-application.nix:2:24: + 1| # Tests that the result of applying op is forced even if the value is never used + 2| builtins.foldl' (_: f: f null) null [ + | ^ + 3| (_: throw "Not the final value, but is still forced!") … while calling anonymous lambda - at /pwd/lang/eval-fail-foldlStrict-strict-op-application.nix:5:6: - 4| null - 5| [ (_: throw "Not the final value, but is still forced!") (_: 23) ] - | ^ - 6| + at /pwd/lang/eval-fail-foldlStrict-strict-op-application.nix:3:4: + 2| builtins.foldl' (_: f: f null) null [ + 3| (_: throw "Not the final value, but is still forced!") + | ^ + 4| (_: 23) … while calling the 'throw' builtin - at /pwd/lang/eval-fail-foldlStrict-strict-op-application.nix:5:9: - 4| null - 5| [ (_: throw "Not the final value, but is still forced!") (_: 23) ] - | ^ - 6| + at /pwd/lang/eval-fail-foldlStrict-strict-op-application.nix:3:7: + 2| builtins.foldl' (_: f: f null) null [ + 3| (_: throw "Not the final value, but is still forced!") + | ^ + 4| (_: 23) error: Not the final value, but is still forced! diff --git a/tests/functional/lang/eval-fail-foldlStrict-strict-op-application.nix b/tests/functional/lang/eval-fail-foldlStrict-strict-op-application.nix index 1620cc76eeb..f85486d441e 100644 --- a/tests/functional/lang/eval-fail-foldlStrict-strict-op-application.nix +++ b/tests/functional/lang/eval-fail-foldlStrict-strict-op-application.nix @@ -1,5 +1,5 @@ # Tests that the result of applying op is forced even if the value is never used -builtins.foldl' - (_: f: f null) - null - [ (_: throw "Not the final value, but is still forced!") (_: 23) ] +builtins.foldl' (_: f: f null) null [ + (_: throw "Not the final value, but is still forced!") + (_: 23) +] diff --git a/tests/functional/lang/eval-fail-fromJSON-keyWithNullByte.err.exp b/tests/functional/lang/eval-fail-fromJSON-keyWithNullByte.err.exp new file mode 100644 index 00000000000..a16192c5983 --- /dev/null +++ b/tests/functional/lang/eval-fail-fromJSON-keyWithNullByte.err.exp @@ -0,0 +1,8 @@ +error: + … while calling the 'fromJSON' builtin + at /pwd/lang/eval-fail-fromJSON-keyWithNullByte.nix:1:1: + 1| builtins.fromJSON ''{"a\u0000b": 1}'' + | ^ + 2| + + error: input string 'aâ€b' cannot be represented as Nix string because it contains null bytes diff --git a/tests/functional/lang/eval-fail-fromJSON-keyWithNullByte.nix b/tests/functional/lang/eval-fail-fromJSON-keyWithNullByte.nix new file mode 100644 index 00000000000..ffaa6a97d22 --- /dev/null +++ b/tests/functional/lang/eval-fail-fromJSON-keyWithNullByte.nix @@ -0,0 +1 @@ +builtins.fromJSON ''{"a\u0000b": 1}'' diff --git a/tests/functional/lang/eval-fail-fromJSON-valueWithNullByte.err.exp b/tests/functional/lang/eval-fail-fromJSON-valueWithNullByte.err.exp new file mode 100644 index 00000000000..c5c08e6ff5d --- /dev/null +++ b/tests/functional/lang/eval-fail-fromJSON-valueWithNullByte.err.exp @@ -0,0 +1,8 @@ +error: + … while calling the 'fromJSON' builtin + at /pwd/lang/eval-fail-fromJSON-valueWithNullByte.nix:1:1: + 1| builtins.fromJSON ''"a\u0000b"'' + | ^ + 2| + + error: input string 'aâ€b' cannot be represented as Nix string because it contains null bytes diff --git a/tests/functional/lang/eval-fail-fromJSON-valueWithNullByte.nix b/tests/functional/lang/eval-fail-fromJSON-valueWithNullByte.nix new file mode 100644 index 00000000000..c71ab990ded --- /dev/null +++ b/tests/functional/lang/eval-fail-fromJSON-valueWithNullByte.nix @@ -0,0 +1 @@ +builtins.fromJSON ''"a\u0000b"'' diff --git a/tests/functional/lang/eval-fail-fromTOML-keyWithNullByte.err.exp b/tests/functional/lang/eval-fail-fromTOML-keyWithNullByte.err.exp new file mode 100644 index 00000000000..dc2180f0bc0 --- /dev/null +++ b/tests/functional/lang/eval-fail-fromTOML-keyWithNullByte.err.exp @@ -0,0 +1,8 @@ +error: + … while calling the 'fromTOML' builtin + at /pwd/lang/eval-fail-fromTOML-keyWithNullByte.nix:1:1: + 1| builtins.fromTOML ''"a\u0000b" = 1'' + | ^ + 2| + + error: while parsing TOML: error: input string 'aâ€b' cannot be represented as Nix string because it contains null bytes diff --git a/tests/functional/lang/eval-fail-fromTOML-keyWithNullByte.nix b/tests/functional/lang/eval-fail-fromTOML-keyWithNullByte.nix new file mode 100644 index 00000000000..b622dc4dcdc --- /dev/null +++ b/tests/functional/lang/eval-fail-fromTOML-keyWithNullByte.nix @@ -0,0 +1 @@ +builtins.fromTOML ''"a\u0000b" = 1'' diff --git a/tests/functional/lang/eval-fail-fromTOML-valueWithNullByte.err.exp b/tests/functional/lang/eval-fail-fromTOML-valueWithNullByte.err.exp new file mode 100644 index 00000000000..0235692a8ed --- /dev/null +++ b/tests/functional/lang/eval-fail-fromTOML-valueWithNullByte.err.exp @@ -0,0 +1,8 @@ +error: + … while calling the 'fromTOML' builtin + at /pwd/lang/eval-fail-fromTOML-valueWithNullByte.nix:1:1: + 1| builtins.fromTOML ''k = "a\u0000b"'' + | ^ + 2| + + error: while parsing TOML: error: input string 'aâ€b' cannot be represented as Nix string because it contains null bytes diff --git a/tests/functional/lang/eval-fail-fromTOML-valueWithNullByte.nix b/tests/functional/lang/eval-fail-fromTOML-valueWithNullByte.nix new file mode 100644 index 00000000000..183cab6b3b6 --- /dev/null +++ b/tests/functional/lang/eval-fail-fromTOML-valueWithNullByte.nix @@ -0,0 +1 @@ +builtins.fromTOML ''k = "a\u0000b"'' diff --git a/tests/functional/lang/eval-fail-hashfile-missing.err.exp b/tests/functional/lang/eval-fail-hashfile-missing.err.exp index 1e465392744..0d3747a6d57 100644 --- a/tests/functional/lang/eval-fail-hashfile-missing.err.exp +++ b/tests/functional/lang/eval-fail-hashfile-missing.err.exp @@ -1,10 +1,10 @@ error: … while calling the 'toString' builtin - at /pwd/lang/eval-fail-hashfile-missing.nix:4:3: - 3| in - 4| toString (builtins.concatLists (map (hash: map (builtins.hashFile hash) paths) ["md5" "sha1" "sha256" "sha512"])) - | ^ - 5| + at /pwd/lang/eval-fail-hashfile-missing.nix:7:1: + 6| in + 7| toString ( + | ^ + 8| builtins.concatLists ( … while evaluating the first argument passed to builtins.toString diff --git a/tests/functional/lang/eval-fail-hashfile-missing.nix b/tests/functional/lang/eval-fail-hashfile-missing.nix index ce098b82380..0f2872b7155 100644 --- a/tests/functional/lang/eval-fail-hashfile-missing.nix +++ b/tests/functional/lang/eval-fail-hashfile-missing.nix @@ -1,5 +1,16 @@ let - paths = [ ./this-file-is-definitely-not-there-7392097 "/and/neither/is/this/37293620" ]; + paths = [ + ./this-file-is-definitely-not-there-7392097 + "/and/neither/is/this/37293620" + ]; in - toString (builtins.concatLists (map (hash: map (builtins.hashFile hash) paths) ["md5" "sha1" "sha256" "sha512"])) - +toString ( + builtins.concatLists ( + map (hash: map (builtins.hashFile hash) paths) [ + "md5" + "sha1" + "sha256" + "sha512" + ] + ) +) diff --git a/tests/functional/lang/eval-fail-list.err.exp b/tests/functional/lang/eval-fail-list.err.exp index d492f8bd2e4..8b21e9a3715 100644 --- a/tests/functional/lang/eval-fail-list.err.exp +++ b/tests/functional/lang/eval-fail-list.err.exp @@ -1,8 +1,8 @@ error: … while evaluating one of the elements to concatenate - at /pwd/lang/eval-fail-list.nix:1:2: - 1| 8++1 - | ^ + at /pwd/lang/eval-fail-list.nix:1:3: + 1| 8 ++ 1 + | ^ 2| error: expected a list but found an integer: 8 diff --git a/tests/functional/lang/eval-fail-list.nix b/tests/functional/lang/eval-fail-list.nix index fa749f2f740..14eb4efa9f6 100644 --- a/tests/functional/lang/eval-fail-list.nix +++ b/tests/functional/lang/eval-fail-list.nix @@ -1 +1 @@ -8++1 +8 ++ 1 diff --git a/tests/functional/lang/eval-fail-missing-arg-import.err.exp b/tests/functional/lang/eval-fail-missing-arg-import.err.exp new file mode 100644 index 00000000000..45774f0032d --- /dev/null +++ b/tests/functional/lang/eval-fail-missing-arg-import.err.exp @@ -0,0 +1,12 @@ +error: + … from call site + at /pwd/lang/eval-fail-missing-arg-import.nix:1:1: + 1| import ./non-eval-trivial-lambda-formals.nix { } + | ^ + 2| + + error: function 'anonymous lambda' called without required argument 'a' + at /pwd/lang/non-eval-trivial-lambda-formals.nix:1:1: + 1| { a }: a + | ^ + 2| diff --git a/tests/functional/lang/eval-fail-missing-arg-import.nix b/tests/functional/lang/eval-fail-missing-arg-import.nix new file mode 100644 index 00000000000..7cb33f2b516 --- /dev/null +++ b/tests/functional/lang/eval-fail-missing-arg-import.nix @@ -0,0 +1 @@ +import ./non-eval-trivial-lambda-formals.nix { } diff --git a/tests/functional/lang/eval-fail-missing-arg.err.exp b/tests/functional/lang/eval-fail-missing-arg.err.exp index 3b162fe1b60..d5a66d2c5ea 100644 --- a/tests/functional/lang/eval-fail-missing-arg.err.exp +++ b/tests/functional/lang/eval-fail-missing-arg.err.exp @@ -1,12 +1,13 @@ error: … from call site at /pwd/lang/eval-fail-missing-arg.nix:1:1: - 1| ({x, y, z}: x + y + z) {x = "foo"; z = "bar";} + 1| ( | ^ - 2| + 2| { error: function 'anonymous lambda' called without required argument 'y' - at /pwd/lang/eval-fail-missing-arg.nix:1:2: - 1| ({x, y, z}: x + y + z) {x = "foo"; z = "bar";} - | ^ - 2| + at /pwd/lang/eval-fail-missing-arg.nix:2:3: + 1| ( + 2| { + | ^ + 3| x, diff --git a/tests/functional/lang/eval-fail-missing-arg.nix b/tests/functional/lang/eval-fail-missing-arg.nix index c4be9797c53..9037aa40a54 100644 --- a/tests/functional/lang/eval-fail-missing-arg.nix +++ b/tests/functional/lang/eval-fail-missing-arg.nix @@ -1 +1,12 @@ -({x, y, z}: x + y + z) {x = "foo"; z = "bar";} +( + { + x, + y, + z, + }: + x + y + z +) + { + x = "foo"; + z = "bar"; + } diff --git a/tests/functional/lang/eval-fail-mutual-recursion.err.exp b/tests/functional/lang/eval-fail-mutual-recursion.err.exp index c034afcd5e0..9d84aa43f0f 100644 --- a/tests/functional/lang/eval-fail-mutual-recursion.err.exp +++ b/tests/functional/lang/eval-fail-mutual-recursion.err.exp @@ -1,64 +1,64 @@ error: … from call site - at /pwd/lang/eval-fail-mutual-recursion.nix:36:3: - 35| in - 36| throwAfterA true 10 - | ^ - 37| + at /pwd/lang/eval-fail-mutual-recursion.nix:40:1: + 39| in + 40| throwAfterA true 10 + | ^ + 41| … while calling 'throwAfterA' - at /pwd/lang/eval-fail-mutual-recursion.nix:29:26: - 28| - 29| throwAfterA = recurse: n: - | ^ - 30| if n > 0 + at /pwd/lang/eval-fail-mutual-recursion.nix:32:14: + 31| throwAfterA = + 32| recurse: n: + | ^ + 33| if n > 0 then … from call site - at /pwd/lang/eval-fail-mutual-recursion.nix:31:10: - 30| if n > 0 - 31| then throwAfterA recurse (n - 1) - | ^ - 32| else if recurse + at /pwd/lang/eval-fail-mutual-recursion.nix:34:7: + 33| if n > 0 then + 34| throwAfterA recurse (n - 1) + | ^ + 35| else if recurse then (19 duplicate frames omitted) … from call site - at /pwd/lang/eval-fail-mutual-recursion.nix:33:10: - 32| else if recurse - 33| then throwAfterB true 10 - | ^ - 34| else throw "Uh oh!"; + at /pwd/lang/eval-fail-mutual-recursion.nix:36:7: + 35| else if recurse then + 36| throwAfterB true 10 + | ^ + 37| else … while calling 'throwAfterB' - at /pwd/lang/eval-fail-mutual-recursion.nix:22:26: - 21| let - 22| throwAfterB = recurse: n: - | ^ - 23| if n > 0 + at /pwd/lang/eval-fail-mutual-recursion.nix:23:14: + 22| throwAfterB = + 23| recurse: n: + | ^ + 24| if n > 0 then … from call site - at /pwd/lang/eval-fail-mutual-recursion.nix:24:10: - 23| if n > 0 - 24| then throwAfterB recurse (n - 1) - | ^ - 25| else if recurse + at /pwd/lang/eval-fail-mutual-recursion.nix:25:7: + 24| if n > 0 then + 25| throwAfterB recurse (n - 1) + | ^ + 26| else if recurse then (19 duplicate frames omitted) … from call site - at /pwd/lang/eval-fail-mutual-recursion.nix:26:10: - 25| else if recurse - 26| then throwAfterA false 10 - | ^ - 27| else throw "Uh oh!"; + at /pwd/lang/eval-fail-mutual-recursion.nix:27:7: + 26| else if recurse then + 27| throwAfterA false 10 + | ^ + 28| else (21 duplicate frames omitted) … while calling the 'throw' builtin - at /pwd/lang/eval-fail-mutual-recursion.nix:34:10: - 33| then throwAfterB true 10 - 34| else throw "Uh oh!"; - | ^ - 35| in + at /pwd/lang/eval-fail-mutual-recursion.nix:38:7: + 37| else + 38| throw "Uh oh!"; + | ^ + 39| in error: Uh oh! diff --git a/tests/functional/lang/eval-fail-mutual-recursion.nix b/tests/functional/lang/eval-fail-mutual-recursion.nix index d090d3158a3..421e464dd86 100644 --- a/tests/functional/lang/eval-fail-mutual-recursion.nix +++ b/tests/functional/lang/eval-fail-mutual-recursion.nix @@ -19,18 +19,22 @@ # - a few frames of A (skip the rest) # - a few frames of B (skip the rest, _and_ skip the remaining frames of A) let - throwAfterB = recurse: n: - if n > 0 - then throwAfterB recurse (n - 1) - else if recurse - then throwAfterA false 10 - else throw "Uh oh!"; + throwAfterB = + recurse: n: + if n > 0 then + throwAfterB recurse (n - 1) + else if recurse then + throwAfterA false 10 + else + throw "Uh oh!"; - throwAfterA = recurse: n: - if n > 0 - then throwAfterA recurse (n - 1) - else if recurse - then throwAfterB true 10 - else throw "Uh oh!"; + throwAfterA = + recurse: n: + if n > 0 then + throwAfterA recurse (n - 1) + else if recurse then + throwAfterB true 10 + else + throw "Uh oh!"; in - throwAfterA true 10 +throwAfterA true 10 diff --git a/tests/functional/lang/eval-fail-nested-list-items.err.exp b/tests/functional/lang/eval-fail-nested-list-items.err.exp index 90d43906165..1169b8326ca 100644 --- a/tests/functional/lang/eval-fail-nested-list-items.err.exp +++ b/tests/functional/lang/eval-fail-nested-list-items.err.exp @@ -1,9 +1,9 @@ error: … while evaluating a path segment - at /pwd/lang/eval-fail-nested-list-items.nix:11:6: - 10| - 11| "" + (let v = [ [ 1 2 3 4 5 6 7 8 ] [1 2 3 4]]; in builtins.deepSeq v v) - | ^ - 12| + at /pwd/lang/eval-fail-nested-list-items.nix:12:3: + 11| "" + 12| + ( + | ^ + 13| let error: cannot coerce a list to a string: [ [ 1 2 3 4 5 6 7 8 ] [ 1 «3 items elided» ] ] diff --git a/tests/functional/lang/eval-fail-nested-list-items.nix b/tests/functional/lang/eval-fail-nested-list-items.nix index af45b1dd49a..d0aa1b5d3b9 100644 --- a/tests/functional/lang/eval-fail-nested-list-items.nix +++ b/tests/functional/lang/eval-fail-nested-list-items.nix @@ -8,4 +8,27 @@ # # error: cannot coerce a list to a string: [ [ 1 2 3 4 5 6 7 8 ] [ 1 «4294967290 items elided» ] ] -"" + (let v = [ [ 1 2 3 4 5 6 7 8 ] [1 2 3 4]]; in builtins.deepSeq v v) +"" ++ ( + let + v = [ + [ + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + ] + [ + 1 + 2 + 3 + 4 + ] + ]; + in + builtins.deepSeq v v +) diff --git a/tests/functional/lang/eval-fail-not-throws.err.exp b/tests/functional/lang/eval-fail-not-throws.err.exp index fc81f7277e1..b49ed7b0048 100644 --- a/tests/functional/lang/eval-fail-not-throws.err.exp +++ b/tests/functional/lang/eval-fail-not-throws.err.exp @@ -1,14 +1,14 @@ error: … in the argument of the not operator - at /pwd/lang/eval-fail-not-throws.nix:1:4: - 1| ! (throw "uh oh!") - | ^ + at /pwd/lang/eval-fail-not-throws.nix:1:3: + 1| !(throw "uh oh!") + | ^ 2| … while calling the 'throw' builtin - at /pwd/lang/eval-fail-not-throws.nix:1:4: - 1| ! (throw "uh oh!") - | ^ + at /pwd/lang/eval-fail-not-throws.nix:1:3: + 1| !(throw "uh oh!") + | ^ 2| error: uh oh! diff --git a/tests/functional/lang/eval-fail-not-throws.nix b/tests/functional/lang/eval-fail-not-throws.nix index a74ce4ebeea..2e024738b68 100644 --- a/tests/functional/lang/eval-fail-not-throws.nix +++ b/tests/functional/lang/eval-fail-not-throws.nix @@ -1 +1 @@ -! (throw "uh oh!") +!(throw "uh oh!") diff --git a/tests/functional/lang/eval-fail-overflowing-add.err.exp b/tests/functional/lang/eval-fail-overflowing-add.err.exp index 6458cf1c933..5a77e9c9d97 100644 --- a/tests/functional/lang/eval-fail-overflowing-add.err.exp +++ b/tests/functional/lang/eval-fail-overflowing-add.err.exp @@ -1,6 +1,6 @@ error: integer overflow in adding 9223372036854775807 + 1 - at /pwd/lang/eval-fail-overflowing-add.nix:4:8: - 3| b = 1; - 4| in a + b - | ^ - 5| + at /pwd/lang/eval-fail-overflowing-add.nix:5:5: + 4| in + 5| a + b + | ^ + 6| diff --git a/tests/functional/lang/eval-fail-overflowing-add.nix b/tests/functional/lang/eval-fail-overflowing-add.nix index 24258fc200e..9e1e8aa7571 100644 --- a/tests/functional/lang/eval-fail-overflowing-add.nix +++ b/tests/functional/lang/eval-fail-overflowing-add.nix @@ -1,4 +1,5 @@ let a = 9223372036854775807; b = 1; -in a + b +in +a + b diff --git a/tests/functional/lang/eval-fail-overflowing-div.err.exp b/tests/functional/lang/eval-fail-overflowing-div.err.exp index 8ce07d4d662..812c6056b76 100644 --- a/tests/functional/lang/eval-fail-overflowing-div.err.exp +++ b/tests/functional/lang/eval-fail-overflowing-div.err.exp @@ -1,23 +1,23 @@ error: … while calling the 'seq' builtin - at /pwd/lang/eval-fail-overflowing-div.nix:7:4: - 6| b = -1; - 7| in builtins.seq intMin (builtins.seq b (intMin / b)) - | ^ - 8| + at /pwd/lang/eval-fail-overflowing-div.nix:8:1: + 7| in + 8| builtins.seq intMin (builtins.seq b (intMin / b)) + | ^ + 9| … while calling the 'seq' builtin - at /pwd/lang/eval-fail-overflowing-div.nix:7:25: - 6| b = -1; - 7| in builtins.seq intMin (builtins.seq b (intMin / b)) - | ^ - 8| + at /pwd/lang/eval-fail-overflowing-div.nix:8:22: + 7| in + 8| builtins.seq intMin (builtins.seq b (intMin / b)) + | ^ + 9| … while calling the 'div' builtin - at /pwd/lang/eval-fail-overflowing-div.nix:7:48: - 6| b = -1; - 7| in builtins.seq intMin (builtins.seq b (intMin / b)) - | ^ - 8| + at /pwd/lang/eval-fail-overflowing-div.nix:8:45: + 7| in + 8| builtins.seq intMin (builtins.seq b (intMin / b)) + | ^ + 9| error: integer overflow in dividing -9223372036854775808 / -1 diff --git a/tests/functional/lang/eval-fail-overflowing-div.nix b/tests/functional/lang/eval-fail-overflowing-div.nix index 44fbe9d7e31..e21b0b2e57d 100644 --- a/tests/functional/lang/eval-fail-overflowing-div.nix +++ b/tests/functional/lang/eval-fail-overflowing-div.nix @@ -4,4 +4,5 @@ let # of range intMin = -9223372036854775807 - 1; b = -1; -in builtins.seq intMin (builtins.seq b (intMin / b)) +in +builtins.seq intMin (builtins.seq b (intMin / b)) diff --git a/tests/functional/lang/eval-fail-overflowing-mul.err.exp b/tests/functional/lang/eval-fail-overflowing-mul.err.exp index f42b39d4db9..aaae4b7bd86 100644 --- a/tests/functional/lang/eval-fail-overflowing-mul.err.exp +++ b/tests/functional/lang/eval-fail-overflowing-mul.err.exp @@ -1,16 +1,16 @@ error: … while calling the 'mul' builtin - at /pwd/lang/eval-fail-overflowing-mul.nix:3:10: - 2| a = 4294967297; - 3| in a * a * a - | ^ - 4| + at /pwd/lang/eval-fail-overflowing-mul.nix:4:7: + 3| in + 4| a * a * a + | ^ + 5| … while calling the 'mul' builtin - at /pwd/lang/eval-fail-overflowing-mul.nix:3:6: - 2| a = 4294967297; - 3| in a * a * a - | ^ - 4| + at /pwd/lang/eval-fail-overflowing-mul.nix:4:3: + 3| in + 4| a * a * a + | ^ + 5| error: integer overflow in multiplying 4294967297 * 4294967297 diff --git a/tests/functional/lang/eval-fail-overflowing-mul.nix b/tests/functional/lang/eval-fail-overflowing-mul.nix index 6081d9c7b14..95b1375bb01 100644 --- a/tests/functional/lang/eval-fail-overflowing-mul.nix +++ b/tests/functional/lang/eval-fail-overflowing-mul.nix @@ -1,3 +1,4 @@ let a = 4294967297; -in a * a * a +in +a * a * a diff --git a/tests/functional/lang/eval-fail-overflowing-sub.err.exp b/tests/functional/lang/eval-fail-overflowing-sub.err.exp index 66a3a03f885..5904c8dcc9d 100644 --- a/tests/functional/lang/eval-fail-overflowing-sub.err.exp +++ b/tests/functional/lang/eval-fail-overflowing-sub.err.exp @@ -1,9 +1,9 @@ error: … while calling the 'sub' builtin - at /pwd/lang/eval-fail-overflowing-sub.nix:4:6: - 3| b = 2; - 4| in a - b - | ^ - 5| + at /pwd/lang/eval-fail-overflowing-sub.nix:5:3: + 4| in + 5| a - b + | ^ + 6| error: integer overflow in subtracting -9223372036854775807 - 2 diff --git a/tests/functional/lang/eval-fail-overflowing-sub.nix b/tests/functional/lang/eval-fail-overflowing-sub.nix index 229b8c6d264..4f0203a6da5 100644 --- a/tests/functional/lang/eval-fail-overflowing-sub.nix +++ b/tests/functional/lang/eval-fail-overflowing-sub.nix @@ -1,4 +1,5 @@ let a = -9223372036854775807; b = 2; -in a - b +in +a - b diff --git a/tests/functional/lang/eval-fail-recursion.err.exp b/tests/functional/lang/eval-fail-recursion.err.exp index 19380dc6536..8bfb4e12e47 100644 --- a/tests/functional/lang/eval-fail-recursion.err.exp +++ b/tests/functional/lang/eval-fail-recursion.err.exp @@ -1,12 +1,14 @@ error: … in the right operand of the update (//) operator - at /pwd/lang/eval-fail-recursion.nix:1:12: - 1| let a = {} // a; in a.foo - | ^ - 2| + at /pwd/lang/eval-fail-recursion.nix:2:11: + 1| let + 2| a = { } // a; + | ^ + 3| in error: infinite recursion encountered - at /pwd/lang/eval-fail-recursion.nix:1:15: - 1| let a = {} // a; in a.foo - | ^ - 2| + at /pwd/lang/eval-fail-recursion.nix:2:14: + 1| let + 2| a = { } // a; + | ^ + 3| in diff --git a/tests/functional/lang/eval-fail-recursion.nix b/tests/functional/lang/eval-fail-recursion.nix index 075b5ed066b..88718a6e507 100644 --- a/tests/functional/lang/eval-fail-recursion.nix +++ b/tests/functional/lang/eval-fail-recursion.nix @@ -1 +1,4 @@ -let a = {} // a; in a.foo +let + a = { } // a; +in +a.foo diff --git a/tests/functional/lang/eval-fail-remove.err.exp b/tests/functional/lang/eval-fail-remove.err.exp index 292b3c3f33a..0e087688a25 100644 --- a/tests/functional/lang/eval-fail-remove.err.exp +++ b/tests/functional/lang/eval-fail-remove.err.exp @@ -1,15 +1,15 @@ error: … while evaluating the attribute 'body' - at /pwd/lang/eval-fail-remove.nix:4:3: - 3| - 4| body = (removeAttrs attrs ["x"]).x; + at /pwd/lang/eval-fail-remove.nix:7:3: + 6| + 7| body = (removeAttrs attrs [ "x" ]).x; | ^ - 5| } + 8| } error: attribute 'x' missing - at /pwd/lang/eval-fail-remove.nix:4:10: - 3| - 4| body = (removeAttrs attrs ["x"]).x; + at /pwd/lang/eval-fail-remove.nix:7:10: + 6| + 7| body = (removeAttrs attrs [ "x" ]).x; | ^ - 5| } + 8| } Did you mean y? diff --git a/tests/functional/lang/eval-fail-remove.nix b/tests/functional/lang/eval-fail-remove.nix index 539e0eb0a6f..9de066abe73 100644 --- a/tests/functional/lang/eval-fail-remove.nix +++ b/tests/functional/lang/eval-fail-remove.nix @@ -1,5 +1,8 @@ let { - attrs = {x = 123; y = 456;}; + attrs = { + x = 123; + y = 456; + }; - body = (removeAttrs attrs ["x"]).x; -} \ No newline at end of file + body = (removeAttrs attrs [ "x" ]).x; +} diff --git a/tests/functional/lang/eval-fail-scope-5.err.exp b/tests/functional/lang/eval-fail-scope-5.err.exp index b0b05cad737..6edc85f4f16 100644 --- a/tests/functional/lang/eval-fail-scope-5.err.exp +++ b/tests/functional/lang/eval-fail-scope-5.err.exp @@ -1,28 +1,28 @@ error: … while evaluating the attribute 'body' - at /pwd/lang/eval-fail-scope-5.nix:8:3: - 7| - 8| body = f {}; + at /pwd/lang/eval-fail-scope-5.nix:13:3: + 12| + 13| body = f { }; | ^ - 9| + 14| … from call site - at /pwd/lang/eval-fail-scope-5.nix:8:10: - 7| - 8| body = f {}; + at /pwd/lang/eval-fail-scope-5.nix:13:10: + 12| + 13| body = f { }; | ^ - 9| + 14| … while calling 'f' - at /pwd/lang/eval-fail-scope-5.nix:6:7: - 5| - 6| f = {x ? y, y ? x}: x + y; - | ^ - 7| + at /pwd/lang/eval-fail-scope-5.nix:7:5: + 6| f = + 7| { + | ^ + 8| x ? y, error: infinite recursion encountered - at /pwd/lang/eval-fail-scope-5.nix:6:12: - 5| - 6| f = {x ? y, y ? x}: x + y; - | ^ - 7| + at /pwd/lang/eval-fail-scope-5.nix:8:11: + 7| { + 8| x ? y, + | ^ + 9| y ? x, diff --git a/tests/functional/lang/eval-fail-scope-5.nix b/tests/functional/lang/eval-fail-scope-5.nix index f89a65a99be..ef6f1bb640e 100644 --- a/tests/functional/lang/eval-fail-scope-5.nix +++ b/tests/functional/lang/eval-fail-scope-5.nix @@ -3,8 +3,13 @@ let { x = "a"; y = "b"; - f = {x ? y, y ? x}: x + y; - - body = f {}; + f = + { + x ? y, + y ? x, + }: + x + y; + + body = f { }; } diff --git a/tests/functional/lang/eval-fail-string-nul-1.err.exp b/tests/functional/lang/eval-fail-string-nul-1.err.exp new file mode 100644 index 00000000000..2dfbea0635c Binary files /dev/null and b/tests/functional/lang/eval-fail-string-nul-1.err.exp differ diff --git a/tests/functional/lang/eval-fail-string-nul-1.nix b/tests/functional/lang/eval-fail-string-nul-1.nix new file mode 100644 index 00000000000..36894091711 Binary files /dev/null and b/tests/functional/lang/eval-fail-string-nul-1.nix differ diff --git a/tests/functional/lang/eval-fail-string-nul-2.err.exp b/tests/functional/lang/eval-fail-string-nul-2.err.exp new file mode 100644 index 00000000000..b1cae5325d9 Binary files /dev/null and b/tests/functional/lang/eval-fail-string-nul-2.err.exp differ diff --git a/tests/functional/lang/eval-fail-string-nul-2.nix b/tests/functional/lang/eval-fail-string-nul-2.nix new file mode 100644 index 00000000000..fd6b3258a5e Binary files /dev/null and b/tests/functional/lang/eval-fail-string-nul-2.nix differ diff --git a/tests/functional/lang/eval-fail-toJSON-non-utf-8.err.exp b/tests/functional/lang/eval-fail-toJSON-non-utf-8.err.exp new file mode 100644 index 00000000000..129d58bcb45 --- /dev/null +++ b/tests/functional/lang/eval-fail-toJSON-non-utf-8.err.exp @@ -0,0 +1,8 @@ +error: + … while calling the 'toJSON' builtin + at /pwd/lang/eval-fail-toJSON-non-utf-8.nix:1:1: + 1| builtins.toJSON "_invalid UTF-8: ÿ_" + | ^ + 2| + + error: JSON serialization error: [json.exception.type_error.316] invalid UTF-8 byte at index 16: 0xFF diff --git a/tests/functional/lang/eval-fail-toJSON-non-utf-8.nix b/tests/functional/lang/eval-fail-toJSON-non-utf-8.nix new file mode 100644 index 00000000000..bd1f74de799 --- /dev/null +++ b/tests/functional/lang/eval-fail-toJSON-non-utf-8.nix @@ -0,0 +1 @@ +builtins.toJSON "_invalid UTF-8: ÿ_" diff --git a/tests/functional/lang/eval-fail-undeclared-arg-import.err.exp b/tests/functional/lang/eval-fail-undeclared-arg-import.err.exp new file mode 100644 index 00000000000..ca797d3eca2 --- /dev/null +++ b/tests/functional/lang/eval-fail-undeclared-arg-import.err.exp @@ -0,0 +1,13 @@ +error: + … from call site + at /pwd/lang/eval-fail-undeclared-arg-import.nix:1:1: + 1| import ./non-eval-trivial-lambda-formals.nix { + | ^ + 2| a = "a"; + + error: function 'anonymous lambda' called with unexpected argument 'b' + at /pwd/lang/non-eval-trivial-lambda-formals.nix:1:1: + 1| { a }: a + | ^ + 2| + Did you mean a? diff --git a/tests/functional/lang/eval-fail-undeclared-arg-import.nix b/tests/functional/lang/eval-fail-undeclared-arg-import.nix new file mode 100644 index 00000000000..e8454c725a7 --- /dev/null +++ b/tests/functional/lang/eval-fail-undeclared-arg-import.nix @@ -0,0 +1,4 @@ +import ./non-eval-trivial-lambda-formals.nix { + a = "a"; + b = "b"; +} diff --git a/tests/functional/lang/eval-fail-undeclared-arg.err.exp b/tests/functional/lang/eval-fail-undeclared-arg.err.exp index 6e13a138eb7..353894d01e6 100644 --- a/tests/functional/lang/eval-fail-undeclared-arg.err.exp +++ b/tests/functional/lang/eval-fail-undeclared-arg.err.exp @@ -1,13 +1,13 @@ error: … from call site at /pwd/lang/eval-fail-undeclared-arg.nix:1:1: - 1| ({x, z}: x + z) {x = "foo"; y = "bla"; z = "bar";} + 1| ({ x, z }: x + z) { | ^ - 2| + 2| x = "foo"; error: function 'anonymous lambda' called with unexpected argument 'y' at /pwd/lang/eval-fail-undeclared-arg.nix:1:2: - 1| ({x, z}: x + z) {x = "foo"; y = "bla"; z = "bar";} + 1| ({ x, z }: x + z) { | ^ - 2| + 2| x = "foo"; Did you mean one of x or z? diff --git a/tests/functional/lang/eval-fail-undeclared-arg.nix b/tests/functional/lang/eval-fail-undeclared-arg.nix index cafdf163627..aca4511bbff 100644 --- a/tests/functional/lang/eval-fail-undeclared-arg.nix +++ b/tests/functional/lang/eval-fail-undeclared-arg.nix @@ -1 +1,5 @@ -({x, z}: x + z) {x = "foo"; y = "bla"; z = "bar";} +({ x, z }: x + z) { + x = "foo"; + y = "bla"; + z = "bar"; +} diff --git a/tests/functional/lang/eval-fail-using-set-as-attr-name.err.exp b/tests/functional/lang/eval-fail-using-set-as-attr-name.err.exp index 4326c965008..9a59f37f35e 100644 --- a/tests/functional/lang/eval-fail-using-set-as-attr-name.err.exp +++ b/tests/functional/lang/eval-fail-using-set-as-attr-name.err.exp @@ -1,14 +1,14 @@ error: … while evaluating an attribute name - at /pwd/lang/eval-fail-using-set-as-attr-name.nix:5:10: - 4| in - 5| attr.${key} - | ^ - 6| + at /pwd/lang/eval-fail-using-set-as-attr-name.nix:7:8: + 6| in + 7| attr.${key} + | ^ + 8| error: expected a string but found a set: { } - at /pwd/lang/eval-fail-using-set-as-attr-name.nix:5:10: - 4| in - 5| attr.${key} - | ^ - 6| + at /pwd/lang/eval-fail-using-set-as-attr-name.nix:7:8: + 6| in + 7| attr.${key} + | ^ + 8| diff --git a/tests/functional/lang/eval-fail-using-set-as-attr-name.nix b/tests/functional/lang/eval-fail-using-set-as-attr-name.nix index 48e071a41cf..96390e35f6a 100644 --- a/tests/functional/lang/eval-fail-using-set-as-attr-name.nix +++ b/tests/functional/lang/eval-fail-using-set-as-attr-name.nix @@ -1,5 +1,7 @@ let - attr = {foo = "bar";}; - key = {}; + attr = { + foo = "bar"; + }; + key = { }; in - attr.${key} +attr.${key} diff --git a/tests/functional/lang/eval-okay-any-all.nix b/tests/functional/lang/eval-okay-any-all.nix index a3f26ea2aa8..643d36cb704 100644 --- a/tests/functional/lang/eval-okay-any-all.nix +++ b/tests/functional/lang/eval-okay-any-all.nix @@ -1,11 +1,34 @@ with builtins; -[ (any (x: x == 1) []) - (any (x: x == 1) [2 3 4]) - (any (x: x == 1) [1 2 3 4]) - (any (x: x == 1) [4 3 2 1]) - (all (x: x == 1) []) - (all (x: x == 1) [1]) - (all (x: x == 1) [1 2 3]) - (all (x: x == 1) [1 1 1]) +[ + (any (x: x == 1) [ ]) + (any (x: x == 1) [ + 2 + 3 + 4 + ]) + (any (x: x == 1) [ + 1 + 2 + 3 + 4 + ]) + (any (x: x == 1) [ + 4 + 3 + 2 + 1 + ]) + (all (x: x == 1) [ ]) + (all (x: x == 1) [ 1 ]) + (all (x: x == 1) [ + 1 + 2 + 3 + ]) + (all (x: x == 1) [ + 1 + 1 + 1 + ]) ] diff --git a/tests/functional/lang/eval-okay-arithmetic.nix b/tests/functional/lang/eval-okay-arithmetic.nix index 7e9e6a0b666..8160b4d84ca 100644 --- a/tests/functional/lang/eval-okay-arithmetic.nix +++ b/tests/functional/lang/eval-okay-arithmetic.nix @@ -2,58 +2,59 @@ with import ./lib.nix; let { - /* Supposedly tail recursive version: + /* + Supposedly tail recursive version: - range_ = accum: first: last: - if first == last then ([first] ++ accum) - else range_ ([first] ++ accum) (builtins.add first 1) last; + range_ = accum: first: last: + if first == last then ([first] ++ accum) + else range_ ([first] ++ accum) (builtins.add first 1) last; - range = range_ []; + range = range_ []; */ x = 12; err = abort "urgh"; - body = sum - [ (sum (range 1 50)) - (123 + 456) - (0 + -10 + -(-11) + -x) - (10 - 7 - -2) - (10 - (6 - -1)) - (10 - 1 + 2) - (3 * 4 * 5) - (56088 / 123 / 2) - (3 + 4 * const 5 0 - 6 / id 2) - - (builtins.bitAnd 12 10) # 0b1100 & 0b1010 = 8 - (builtins.bitOr 12 10) # 0b1100 | 0b1010 = 14 - (builtins.bitXor 12 10) # 0b1100 ^ 0b1010 = 6 - - (if 3 < 7 then 1 else err) - (if 7 < 3 then err else 1) - (if 3 < 3 then err else 1) - - (if 3 <= 7 then 1 else err) - (if 7 <= 3 then err else 1) - (if 3 <= 3 then 1 else err) - - (if 3 > 7 then err else 1) - (if 7 > 3 then 1 else err) - (if 3 > 3 then err else 1) - - (if 3 >= 7 then err else 1) - (if 7 >= 3 then 1 else err) - (if 3 >= 3 then 1 else err) - - (if 2 > 1 == 1 < 2 then 1 else err) - (if 1 + 2 * 3 >= 7 then 1 else err) - (if 1 + 2 * 3 < 7 then err else 1) - - # Not integer, but so what. - (if "aa" < "ab" then 1 else err) - (if "aa" < "aa" then err else 1) - (if "foo" < "foobar" then 1 else err) - ]; + body = sum [ + (sum (range 1 50)) + (123 + 456) + (0 + -10 + -(-11) + -x) + (10 - 7 - -2) + (10 - (6 - -1)) + (10 - 1 + 2) + (3 * 4 * 5) + (56088 / 123 / 2) + (3 + 4 * const 5 0 - 6 / id 2) + + (builtins.bitAnd 12 10) # 0b1100 & 0b1010 = 8 + (builtins.bitOr 12 10) # 0b1100 | 0b1010 = 14 + (builtins.bitXor 12 10) # 0b1100 ^ 0b1010 = 6 + + (if 3 < 7 then 1 else err) + (if 7 < 3 then err else 1) + (if 3 < 3 then err else 1) + + (if 3 <= 7 then 1 else err) + (if 7 <= 3 then err else 1) + (if 3 <= 3 then 1 else err) + + (if 3 > 7 then err else 1) + (if 7 > 3 then 1 else err) + (if 3 > 3 then err else 1) + + (if 3 >= 7 then err else 1) + (if 7 >= 3 then 1 else err) + (if 3 >= 3 then 1 else err) + + (if 2 > 1 == 1 < 2 then 1 else err) + (if 1 + 2 * 3 >= 7 then 1 else err) + (if 1 + 2 * 3 < 7 then err else 1) + + # Not integer, but so what. + (if "aa" < "ab" then 1 else err) + (if "aa" < "aa" then err else 1) + (if "foo" < "foobar" then 1 else err) + ]; } diff --git a/tests/functional/lang/eval-okay-attrnames.nix b/tests/functional/lang/eval-okay-attrnames.nix index e5b26e9f2e3..085e78084b0 100644 --- a/tests/functional/lang/eval-okay-attrnames.nix +++ b/tests/functional/lang/eval-okay-attrnames.nix @@ -2,10 +2,21 @@ with import ./lib.nix; let - attrs = {y = "y"; x = "x"; foo = "foo";} // rec {x = "newx"; bar = x;}; + attrs = + { + y = "y"; + x = "x"; + foo = "foo"; + } + // rec { + x = "newx"; + bar = x; + }; names = builtins.attrNames attrs; values = map (name: builtins.getAttr name attrs) names; -in assert values == builtins.attrValues attrs; concat values +in +assert values == builtins.attrValues attrs; +concat values diff --git a/tests/functional/lang/eval-okay-attrs.nix b/tests/functional/lang/eval-okay-attrs.nix index 810b31a5da9..787b9a933cf 100644 --- a/tests/functional/lang/eval-okay-attrs.nix +++ b/tests/functional/lang/eval-okay-attrs.nix @@ -1,5 +1,20 @@ let { - as = { x = 123; y = 456; } // { z = 789; } // { z = 987; }; + as = + { + x = 123; + y = 456; + } + // { + z = 789; + } + // { + z = 987; + }; - body = if as ? a then as.a else assert as ? z; as.z; + body = + if as ? a then + as.a + else + assert as ? z; + as.z; } diff --git a/tests/functional/lang/eval-okay-attrs2.nix b/tests/functional/lang/eval-okay-attrs2.nix index 9e06b83ac1f..0896f9cf1e1 100644 --- a/tests/functional/lang/eval-okay-attrs2.nix +++ b/tests/functional/lang/eval-okay-attrs2.nix @@ -1,10 +1,23 @@ let { - as = { x = 123; y = 456; } // { z = 789; } // { z = 987; }; + as = + { + x = 123; + y = 456; + } + // { + z = 789; + } + // { + z = 987; + }; A = "a"; Z = "z"; - body = if builtins.hasAttr A as - then builtins.getAttr A as - else assert builtins.hasAttr Z as; builtins.getAttr Z as; + body = + if builtins.hasAttr A as then + builtins.getAttr A as + else + assert builtins.hasAttr Z as; + builtins.getAttr Z as; } diff --git a/tests/functional/lang/eval-okay-attrs3.nix b/tests/functional/lang/eval-okay-attrs3.nix index f29de11fe66..cab345337dd 100644 --- a/tests/functional/lang/eval-okay-attrs3.nix +++ b/tests/functional/lang/eval-okay-attrs3.nix @@ -1,22 +1,22 @@ let - config = - { - services.sshd.enable = true; - services.sshd.port = 22; - services.httpd.port = 80; - hostName = "itchy"; - a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z = "x"; - foo = { - a = "a"; - b.c = "c"; - }; + config = { + services.sshd.enable = true; + services.sshd.port = 22; + services.httpd.port = 80; + hostName = "itchy"; + a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z = "x"; + foo = { + a = "a"; + b.c = "c"; }; + }; in - if config.services.sshd.enable - then "foo ${toString config.services.sshd.port} ${toString config.services.httpd.port} ${config.hostName}" - + "${config.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z}" - + "${config.foo.a}" - + "${config.foo.b.c}" - else "bar" +if config.services.sshd.enable then + "foo ${toString config.services.sshd.port} ${toString config.services.httpd.port} ${config.hostName}" + + "${config.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z}" + + "${config.foo.a}" + + "${config.foo.b.c}" +else + "bar" diff --git a/tests/functional/lang/eval-okay-attrs4.nix b/tests/functional/lang/eval-okay-attrs4.nix index 43ec81210f3..3e43e4bae4f 100644 --- a/tests/functional/lang/eval-okay-attrs4.nix +++ b/tests/functional/lang/eval-okay-attrs4.nix @@ -1,7 +1,20 @@ let - as = { x.y.z = 123; a.b.c = 456; }; + as = { + x.y.z = 123; + a.b.c = 456; + }; bs = null; -in [ (as ? x) (as ? y) (as ? x.y.z) (as ? x.y.z.a) (as ? x.y.a) (as ? a.b.c) (bs ? x) (bs ? x.y.z) ] +in +[ + (as ? x) + (as ? y) + (as ? x.y.z) + (as ? x.y.z.a) + (as ? x.y.a) + (as ? a.b.c) + (bs ? x) + (bs ? x.y.z) +] diff --git a/tests/functional/lang/eval-okay-attrs6.nix b/tests/functional/lang/eval-okay-attrs6.nix index 2e5c85483be..76c94af785a 100644 --- a/tests/functional/lang/eval-okay-attrs6.nix +++ b/tests/functional/lang/eval-okay-attrs6.nix @@ -1,4 +1,6 @@ rec { "${"foo"}" = "bar"; - __overrides = { bar = "qux"; }; + __overrides = { + bar = "qux"; + }; } diff --git a/tests/functional/lang/eval-okay-autoargs.nix b/tests/functional/lang/eval-okay-autoargs.nix index 815f51b1d67..bc82c569b48 100644 --- a/tests/functional/lang/eval-okay-autoargs.nix +++ b/tests/functional/lang/eval-okay-autoargs.nix @@ -4,12 +4,17 @@ let in -{ xyzzy2 ? xyzzy # mutually recursive args -, xyzzy ? "blaat" # will be overridden by --argstr -, fb ? foobar -, lib # will be set by --arg +{ + xyzzy2 ? xyzzy, # mutually recursive args + xyzzy ? "blaat", # will be overridden by --argstr + fb ? foobar, + lib, # will be set by --arg }: { - result = lib.concat [xyzzy xyzzy2 fb]; + result = lib.concat [ + xyzzy + xyzzy2 + fb + ]; } diff --git a/tests/functional/lang/eval-okay-builtins-add.nix b/tests/functional/lang/eval-okay-builtins-add.nix index c841816222a..f678f640f12 100644 --- a/tests/functional/lang/eval-okay-builtins-add.nix +++ b/tests/functional/lang/eval-okay-builtins-add.nix @@ -1,8 +1,8 @@ [ -(builtins.add 2 3) -(builtins.add 2 2) -(builtins.typeOf (builtins.add 2 2)) -("t" + "t") -(builtins.typeOf (builtins.add 2.0 2)) -(builtins.add 2.0 2) + (builtins.add 2 3) + (builtins.add 2 2) + (builtins.typeOf (builtins.add 2 2)) + ("t" + "t") + (builtins.typeOf (builtins.add 2.0 2)) + (builtins.add 2.0 2) ] diff --git a/tests/functional/lang/eval-okay-builtins.nix b/tests/functional/lang/eval-okay-builtins.nix index e9d65e88a81..be4114116f3 100644 --- a/tests/functional/lang/eval-okay-builtins.nix +++ b/tests/functional/lang/eval-okay-builtins.nix @@ -8,5 +8,5 @@ let { y = if builtins ? fnord then builtins.fnord "foo" else ""; body = x + y; - + } diff --git a/tests/functional/lang/eval-okay-callable-attrs.nix b/tests/functional/lang/eval-okay-callable-attrs.nix index 310a030df00..a4c1ace362b 100644 --- a/tests/functional/lang/eval-okay-callable-attrs.nix +++ b/tests/functional/lang/eval-okay-callable-attrs.nix @@ -1 +1,10 @@ -({ __functor = self: x: self.foo && x; foo = false; } // { foo = true; }) true +( + { + __functor = self: x: self.foo && x; + foo = false; + } + // { + foo = true; + } +) + true diff --git a/tests/functional/lang/eval-okay-catattrs.nix b/tests/functional/lang/eval-okay-catattrs.nix index 2c3dc10da52..7ec4ba7aeb2 100644 --- a/tests/functional/lang/eval-okay-catattrs.nix +++ b/tests/functional/lang/eval-okay-catattrs.nix @@ -1 +1,5 @@ -builtins.catAttrs "a" [ { a = 1; } { b = 0; } { a = 2; } ] +builtins.catAttrs "a" [ + { a = 1; } + { b = 0; } + { a = 2; } +] diff --git a/tests/functional/lang/eval-okay-closure.nix b/tests/functional/lang/eval-okay-closure.nix index cccd4dc3573..67c53d08947 100644 --- a/tests/functional/lang/eval-okay-closure.nix +++ b/tests/functional/lang/eval-okay-closure.nix @@ -1,13 +1,25 @@ let closure = builtins.genericClosure { - startSet = [{key = 80;}]; - operator = {key, foo ? false}: - if builtins.lessThan key 0 - then [] - else [{key = builtins.sub key 9;} {key = builtins.sub key 13; foo = true;}]; + startSet = [ { key = 80; } ]; + operator = + { + key, + foo ? false, + }: + if builtins.lessThan key 0 then + [ ] + else + [ + { key = builtins.sub key 9; } + { + key = builtins.sub key 13; + foo = true; + } + ]; }; sort = (import ./lib.nix).sortBy (a: b: builtins.lessThan a.key b.key); -in sort closure +in +sort closure diff --git a/tests/functional/lang/eval-okay-concat.nix b/tests/functional/lang/eval-okay-concat.nix index d158a9bf05b..ce754ca005f 100644 --- a/tests/functional/lang/eval-okay-concat.nix +++ b/tests/functional/lang/eval-okay-concat.nix @@ -1 +1,15 @@ -[1 2 3] ++ [4 5 6] ++ [7 8 9] +[ + 1 + 2 + 3 +] +++ [ + 4 + 5 + 6 +] +++ [ + 7 + 8 + 9 +] diff --git a/tests/functional/lang/eval-okay-concatmap.nix b/tests/functional/lang/eval-okay-concatmap.nix index 97da5d37a41..14b5461319e 100644 --- a/tests/functional/lang/eval-okay-concatmap.nix +++ b/tests/functional/lang/eval-okay-concatmap.nix @@ -1,5 +1,9 @@ with import ./lib.nix; -[ (builtins.concatMap (x: if x / 2 * 2 == x then [] else [ x ]) (range 0 10)) - (builtins.concatMap (x: [x] ++ ["z"]) ["a" "b"]) +[ + (builtins.concatMap (x: if x / 2 * 2 == x then [ ] else [ x ]) (range 0 10)) + (builtins.concatMap (x: [ x ] ++ [ "z" ]) [ + "a" + "b" + ]) ] diff --git a/tests/functional/lang/eval-okay-concatstringssep.nix b/tests/functional/lang/eval-okay-concatstringssep.nix index adc4c41bd55..2270d11b4c4 100644 --- a/tests/functional/lang/eval-okay-concatstringssep.nix +++ b/tests/functional/lang/eval-okay-concatstringssep.nix @@ -1,8 +1,17 @@ with builtins; -[ (concatStringsSep "" []) - (concatStringsSep "" ["foo" "bar" "xyzzy"]) - (concatStringsSep ", " ["foo" "bar" "xyzzy"]) - (concatStringsSep ", " ["foo"]) - (concatStringsSep ", " []) +[ + (concatStringsSep "" [ ]) + (concatStringsSep "" [ + "foo" + "bar" + "xyzzy" + ]) + (concatStringsSep ", " [ + "foo" + "bar" + "xyzzy" + ]) + (concatStringsSep ", " [ "foo" ]) + (concatStringsSep ", " [ ]) ] diff --git a/tests/functional/lang/eval-okay-context-introspection.nix b/tests/functional/lang/eval-okay-context-introspection.nix index 8886cf32e94..5ed99471901 100644 --- a/tests/functional/lang/eval-okay-context-introspection.nix +++ b/tests/functional/lang/eval-okay-context-introspection.nix @@ -3,7 +3,10 @@ let name = "fail"; builder = "/bin/false"; system = "x86_64-linux"; - outputs = [ "out" "foo" ]; + outputs = [ + "out" + "foo" + ]; }; path = "${./eval-okay-context-introspection.nix}"; @@ -13,7 +16,10 @@ let path = true; }; "${builtins.unsafeDiscardStringContext drv.drvPath}" = { - outputs = [ "foo" "out" ]; + outputs = [ + "foo" + "out" + ]; allOutputs = true; }; }; @@ -21,25 +27,22 @@ let combo-path = "${path}${drv.outPath}${drv.foo.outPath}${drv.drvPath}"; legit-context = builtins.getContext combo-path; - reconstructed-path = builtins.appendContext - (builtins.unsafeDiscardStringContext combo-path) - desired-context; + reconstructed-path = builtins.appendContext (builtins.unsafeDiscardStringContext combo-path) desired-context; # Eta rule for strings with context. - etaRule = str: - str == builtins.appendContext - (builtins.unsafeDiscardStringContext str) - (builtins.getContext str); + etaRule = + str: + str == builtins.appendContext (builtins.unsafeDiscardStringContext str) (builtins.getContext str); # Only holds true if string context contains both a `DrvDeep` and # `Opaque` element. - almostEtaRule = str: - str == builtins.addDrvOutputDependencies - (builtins.unsafeDiscardOutputDependency str); + almostEtaRule = + str: str == builtins.addDrvOutputDependencies (builtins.unsafeDiscardOutputDependency str); - addDrvOutputDependencies_idempotent = str: - builtins.addDrvOutputDependencies str == - builtins.addDrvOutputDependencies (builtins.addDrvOutputDependencies str); + addDrvOutputDependencies_idempotent = + str: + builtins.addDrvOutputDependencies str + == builtins.addDrvOutputDependencies (builtins.addDrvOutputDependencies str); rules = str: [ (etaRule str) @@ -47,12 +50,14 @@ let (addDrvOutputDependencies_idempotent str) ]; -in [ +in +[ (legit-context == desired-context) (reconstructed-path == combo-path) (etaRule "foo") (etaRule drv.foo.outPath) -] ++ builtins.concatMap rules [ +] +++ builtins.concatMap rules [ drv.drvPath (builtins.addDrvOutputDependencies drv.drvPath) (builtins.unsafeDiscardOutputDependency drv.drvPath) diff --git a/tests/functional/lang/eval-okay-context.nix b/tests/functional/lang/eval-okay-context.nix index 7b9531cfe9e..102bc22599c 100644 --- a/tests/functional/lang/eval-okay-context.nix +++ b/tests/functional/lang/eval-okay-context.nix @@ -1,6 +1,7 @@ -let s = "foo ${builtins.substring 33 100 (baseNameOf "${./eval-okay-context.nix}")} bar"; +let + s = "foo ${builtins.substring 33 100 (baseNameOf "${./eval-okay-context.nix}")} bar"; in - if s != "foo eval-okay-context.nix bar" - then abort "context not discarded" - else builtins.unsafeDiscardStringContext s - +if s != "foo eval-okay-context.nix bar" then + abort "context not discarded" +else + builtins.unsafeDiscardStringContext s diff --git a/tests/functional/lang/eval-okay-convertHash.nix b/tests/functional/lang/eval-okay-convertHash.nix index a0191ee8df1..6d5074fea23 100644 --- a/tests/functional/lang/eval-okay-convertHash.nix +++ b/tests/functional/lang/eval-okay-convertHash.nix @@ -1,33 +1,131 @@ let - hashAlgos = [ "md5" "md5" "md5" "sha1" "sha1" "sha1" "sha256" "sha256" "sha256" "sha512" "sha512" "sha512" ]; + hashAlgos = [ + "md5" + "md5" + "md5" + "sha1" + "sha1" + "sha1" + "sha256" + "sha256" + "sha256" + "sha512" + "sha512" + "sha512" + ]; hashesBase16 = import ./eval-okay-hashstring.exp; - map2 = f: { fsts, snds }: if fsts == [ ] then [ ] else [ (f (builtins.head fsts) (builtins.head snds)) ] ++ map2 f { fsts = builtins.tail fsts; snds = builtins.tail snds; }; - map2' = f: fsts: snds: map2 f { inherit fsts snds; }; + map2 = + f: + { fsts, snds }: + if fsts == [ ] then + [ ] + else + [ (f (builtins.head fsts) (builtins.head snds)) ] + ++ map2 f { + fsts = builtins.tail fsts; + snds = builtins.tail snds; + }; + map2' = + f: fsts: snds: + map2 f { inherit fsts snds; }; getOutputHashes = hashes: { - hashesBase16 = map2' (hashAlgo: hash: builtins.convertHash { inherit hash hashAlgo; toHashFormat = "base16";}) hashAlgos hashes; - hashesNix32 = map2' (hashAlgo: hash: builtins.convertHash { inherit hash hashAlgo; toHashFormat = "nix32";}) hashAlgos hashes; - hashesBase32 = map2' (hashAlgo: hash: builtins.convertHash { inherit hash hashAlgo; toHashFormat = "base32";}) hashAlgos hashes; - hashesBase64 = map2' (hashAlgo: hash: builtins.convertHash { inherit hash hashAlgo; toHashFormat = "base64";}) hashAlgos hashes; - hashesSRI = map2' (hashAlgo: hash: builtins.convertHash { inherit hash hashAlgo; toHashFormat = "sri" ;}) hashAlgos hashes; + hashesBase16 = map2' ( + hashAlgo: hash: + builtins.convertHash { + inherit hash hashAlgo; + toHashFormat = "base16"; + } + ) hashAlgos hashes; + hashesNix32 = map2' ( + hashAlgo: hash: + builtins.convertHash { + inherit hash hashAlgo; + toHashFormat = "nix32"; + } + ) hashAlgos hashes; + hashesBase32 = map2' ( + hashAlgo: hash: + builtins.convertHash { + inherit hash hashAlgo; + toHashFormat = "base32"; + } + ) hashAlgos hashes; + hashesBase64 = map2' ( + hashAlgo: hash: + builtins.convertHash { + inherit hash hashAlgo; + toHashFormat = "base64"; + } + ) hashAlgos hashes; + hashesSRI = map2' ( + hashAlgo: hash: + builtins.convertHash { + inherit hash hashAlgo; + toHashFormat = "sri"; + } + ) hashAlgos hashes; }; getOutputHashesColon = hashes: { - hashesBase16 = map2' (hashAlgo: hashBody: builtins.convertHash { hash = hashAlgo + ":" + hashBody; toHashFormat = "base16";}) hashAlgos hashes; - hashesNix32 = map2' (hashAlgo: hashBody: builtins.convertHash { hash = hashAlgo + ":" + hashBody; toHashFormat = "nix32";}) hashAlgos hashes; - hashesBase32 = map2' (hashAlgo: hashBody: builtins.convertHash { hash = hashAlgo + ":" + hashBody; toHashFormat = "base32";}) hashAlgos hashes; - hashesBase64 = map2' (hashAlgo: hashBody: builtins.convertHash { hash = hashAlgo + ":" + hashBody; toHashFormat = "base64";}) hashAlgos hashes; - hashesSRI = map2' (hashAlgo: hashBody: builtins.convertHash { hash = hashAlgo + ":" + hashBody; toHashFormat = "sri" ;}) hashAlgos hashes; + hashesBase16 = map2' ( + hashAlgo: hashBody: + builtins.convertHash { + hash = hashAlgo + ":" + hashBody; + toHashFormat = "base16"; + } + ) hashAlgos hashes; + hashesNix32 = map2' ( + hashAlgo: hashBody: + builtins.convertHash { + hash = hashAlgo + ":" + hashBody; + toHashFormat = "nix32"; + } + ) hashAlgos hashes; + hashesBase32 = map2' ( + hashAlgo: hashBody: + builtins.convertHash { + hash = hashAlgo + ":" + hashBody; + toHashFormat = "base32"; + } + ) hashAlgos hashes; + hashesBase64 = map2' ( + hashAlgo: hashBody: + builtins.convertHash { + hash = hashAlgo + ":" + hashBody; + toHashFormat = "base64"; + } + ) hashAlgos hashes; + hashesSRI = map2' ( + hashAlgo: hashBody: + builtins.convertHash { + hash = hashAlgo + ":" + hashBody; + toHashFormat = "sri"; + } + ) hashAlgos hashes; }; outputHashes = getOutputHashes hashesBase16; in # map2'` -assert map2' (s1: s2: s1 + s2) [ "a" "b" ] [ "c" "d" ] == [ "ac" "bd" ]; +assert + map2' (s1: s2: s1 + s2) [ "a" "b" ] [ "c" "d" ] == [ + "ac" + "bd" + ]; # hashesBase16 assert outputHashes.hashesBase16 == hashesBase16; # standard SRI hashes -assert outputHashes.hashesSRI == (map2' (hashAlgo: hashBody: hashAlgo + "-" + hashBody) hashAlgos outputHashes.hashesBase64); +assert + outputHashes.hashesSRI + == (map2' (hashAlgo: hashBody: hashAlgo + "-" + hashBody) hashAlgos outputHashes.hashesBase64); # without prefix assert builtins.all (x: getOutputHashes x == outputHashes) (builtins.attrValues outputHashes); # colon-separated. # Note that colon prefix must not be applied to the standard SRI. e.g. "sha256:sha256-..." is illegal. -assert builtins.all (x: getOutputHashesColon x == outputHashes) (with outputHashes; [ hashesBase16 hashesBase32 hashesBase64 ]); +assert builtins.all (x: getOutputHashesColon x == outputHashes) ( + with outputHashes; + [ + hashesBase16 + hashesBase32 + hashesBase64 + ] +); outputHashes diff --git a/tests/functional/lang/eval-okay-deepseq.nix b/tests/functional/lang/eval-okay-deepseq.nix index 53aa4b1dc25..f9aa5f720f3 100644 --- a/tests/functional/lang/eval-okay-deepseq.nix +++ b/tests/functional/lang/eval-okay-deepseq.nix @@ -1 +1,9 @@ -builtins.deepSeq (let as = { x = 123; y = as; }; in as) 456 +builtins.deepSeq ( + let + as = { + x = 123; + y = as; + }; + in + as +) 456 diff --git a/tests/functional/lang/eval-okay-delayed-with-inherit.nix b/tests/functional/lang/eval-okay-delayed-with-inherit.nix index 84b388c2713..10ce7df13c0 100644 --- a/tests/functional/lang/eval-okay-delayed-with-inherit.nix +++ b/tests/functional/lang/eval-okay-delayed-with-inherit.nix @@ -4,7 +4,10 @@ let name = "a"; system = builtins.currentSystem; builder = "/bin/sh"; - args = [ "-c" "touch $out" ]; + args = [ + "-c" + "touch $out" + ]; inherit b; }; @@ -16,9 +19,13 @@ let name = "b-overridden"; system = builtins.currentSystem; builder = "/bin/sh"; - args = [ "-c" "touch $out" ]; + args = [ + "-c" + "touch $out" + ]; }; }; pkgs = pkgs_ // (packageOverrides pkgs_); -in pkgs.a.b.name +in +pkgs.a.b.name diff --git a/tests/functional/lang/eval-okay-delayed-with.nix b/tests/functional/lang/eval-okay-delayed-with.nix index 3fb023e1cd4..52ec24e12e4 100644 --- a/tests/functional/lang/eval-okay-delayed-with.nix +++ b/tests/functional/lang/eval-okay-delayed-with.nix @@ -5,7 +5,10 @@ let name = "a"; system = builtins.currentSystem; builder = "/bin/sh"; - args = [ "-c" "touch $out" ]; + args = [ + "-c" + "touch $out" + ]; inherit b; }; @@ -13,17 +16,22 @@ let name = "b"; system = builtins.currentSystem; builder = "/bin/sh"; - args = [ "-c" "touch $out" ]; + args = [ + "-c" + "touch $out" + ]; inherit a; }; c = b; }; - packageOverrides = pkgs: with pkgs; { - b = derivation (b.drvAttrs // { name = "${b.name}-overridden"; }); - }; + packageOverrides = + pkgs: with pkgs; { + b = derivation (b.drvAttrs // { name = "${b.name}-overridden"; }); + }; pkgs = pkgs_ // (packageOverrides pkgs_); -in "${pkgs.a.b.name} ${pkgs.c.name} ${pkgs.b.a.name}" +in +"${pkgs.a.b.name} ${pkgs.c.name} ${pkgs.b.a.name}" diff --git a/tests/functional/lang/eval-okay-dynamic-attrs-2.nix b/tests/functional/lang/eval-okay-dynamic-attrs-2.nix index 6d57bf85490..95fe79e2558 100644 --- a/tests/functional/lang/eval-okay-dynamic-attrs-2.nix +++ b/tests/functional/lang/eval-okay-dynamic-attrs-2.nix @@ -1 +1,5 @@ -{ a."${"b"}" = true; a."${"c"}" = false; }.a.b +{ + a."${"b"}" = true; + a."${"c"}" = false; +} +.a.b diff --git a/tests/functional/lang/eval-okay-dynamic-attrs-bare.nix b/tests/functional/lang/eval-okay-dynamic-attrs-bare.nix index 0dbe15e6384..a612bf69dfa 100644 --- a/tests/functional/lang/eval-okay-dynamic-attrs-bare.nix +++ b/tests/functional/lang/eval-okay-dynamic-attrs-bare.nix @@ -2,7 +2,8 @@ let aString = "a"; bString = "b"; -in { +in +{ hasAttrs = { a.b = null; } ? ${aString}.b; selectAttrs = { a.b = true; }.a.${bString}; @@ -11,7 +12,17 @@ in { binds = { ${aString}."${bString}c" = true; }.a.bc; - recBinds = rec { ${bString} = a; a = true; }.b; + recBinds = + rec { + ${bString} = a; + a = true; + } + .b; - multiAttrs = { ${aString} = true; ${bString} = false; }.a; + multiAttrs = + { + ${aString} = true; + ${bString} = false; + } + .a; } diff --git a/tests/functional/lang/eval-okay-dynamic-attrs.nix b/tests/functional/lang/eval-okay-dynamic-attrs.nix index ee02ac7e657..f46e26b992f 100644 --- a/tests/functional/lang/eval-okay-dynamic-attrs.nix +++ b/tests/functional/lang/eval-okay-dynamic-attrs.nix @@ -2,7 +2,8 @@ let aString = "a"; bString = "b"; -in { +in +{ hasAttrs = { a.b = null; } ? "${aString}".b; selectAttrs = { a.b = true; }.a."${bString}"; @@ -11,7 +12,17 @@ in { binds = { "${aString}"."${bString}c" = true; }.a.bc; - recBinds = rec { "${bString}" = a; a = true; }.b; + recBinds = + rec { + "${bString}" = a; + a = true; + } + .b; - multiAttrs = { "${aString}" = true; "${bString}" = false; }.a; + multiAttrs = + { + "${aString}" = true; + "${bString}" = false; + } + .a; } diff --git a/tests/functional/lang/eval-okay-elem.nix b/tests/functional/lang/eval-okay-elem.nix index 71ea7a4ed03..004111dcc69 100644 --- a/tests/functional/lang/eval-okay-elem.nix +++ b/tests/functional/lang/eval-okay-elem.nix @@ -1,6 +1,11 @@ with import ./lib.nix; -let xs = range 10 40; in - -[ (builtins.elem 23 xs) (builtins.elem 42 xs) (builtins.elemAt xs 20) ] +let + xs = range 10 40; +in +[ + (builtins.elem 23 xs) + (builtins.elem 42 xs) + (builtins.elemAt xs 20) +] diff --git a/tests/functional/lang/eval-okay-empty-args.nix b/tests/functional/lang/eval-okay-empty-args.nix index 78c133afdd9..9466749f6ab 100644 --- a/tests/functional/lang/eval-okay-empty-args.nix +++ b/tests/functional/lang/eval-okay-empty-args.nix @@ -1 +1,4 @@ -({}: {x,y,}: "${x}${y}") {} {x = "a"; y = "b";} +({ }: { x, y }: "${x}${y}") { } { + x = "a"; + y = "b"; +} diff --git a/tests/functional/lang/eval-okay-eq-derivations.nix b/tests/functional/lang/eval-okay-eq-derivations.nix index d526cb4a216..ac802f433c7 100644 --- a/tests/functional/lang/eval-okay-eq-derivations.nix +++ b/tests/functional/lang/eval-okay-eq-derivations.nix @@ -1,10 +1,40 @@ let - drvA1 = derivation { name = "a"; builder = "/foo"; system = "i686-linux"; }; - drvA2 = derivation { name = "a"; builder = "/foo"; system = "i686-linux"; }; - drvA3 = derivation { name = "a"; builder = "/foo"; system = "i686-linux"; } // { dummy = 1; }; - - drvC1 = derivation { name = "c"; builder = "/foo"; system = "i686-linux"; }; - drvC2 = derivation { name = "c"; builder = "/bar"; system = "i686-linux"; }; + drvA1 = derivation { + name = "a"; + builder = "/foo"; + system = "i686-linux"; + }; + drvA2 = derivation { + name = "a"; + builder = "/foo"; + system = "i686-linux"; + }; + drvA3 = + derivation { + name = "a"; + builder = "/foo"; + system = "i686-linux"; + } + // { + dummy = 1; + }; -in [ (drvA1 == drvA1) (drvA1 == drvA2) (drvA1 == drvA3) (drvC1 == drvC2) ] + drvC1 = derivation { + name = "c"; + builder = "/foo"; + system = "i686-linux"; + }; + drvC2 = derivation { + name = "c"; + builder = "/bar"; + system = "i686-linux"; + }; + +in +[ + (drvA1 == drvA1) + (drvA1 == drvA2) + (drvA1 == drvA3) + (drvC1 == drvC2) +] diff --git a/tests/functional/lang/eval-okay-eq.nix b/tests/functional/lang/eval-okay-eq.nix index 73d200b3814..21cb08790ca 100644 --- a/tests/functional/lang/eval-okay-eq.nix +++ b/tests/functional/lang/eval-okay-eq.nix @@ -1,3 +1,13 @@ -["foobar" (rec {x = 1; y = x;})] -== -[("foo" + "bar") ({x = 1; y = 1;})] +[ + "foobar" + (rec { + x = 1; + y = x; + }) +] == [ + ("foo" + "bar") + ({ + x = 1; + y = 1; + }) +] diff --git a/tests/functional/lang/eval-okay-filter.nix b/tests/functional/lang/eval-okay-filter.nix index 85109b0d0eb..ef4e490c0fd 100644 --- a/tests/functional/lang/eval-okay-filter.nix +++ b/tests/functional/lang/eval-okay-filter.nix @@ -1,5 +1,8 @@ with import ./lib.nix; -builtins.filter - (x: x / 2 * 2 == x) - (builtins.concatLists [ (range 0 10) (range 100 110) ]) +builtins.filter (x: x / 2 * 2 == x) ( + builtins.concatLists [ + (range 0 10) + (range 100 110) + ] +) diff --git a/tests/functional/lang/eval-okay-flake-ref-to-string.nix b/tests/functional/lang/eval-okay-flake-ref-to-string.nix index dbb4e5b2af4..f477ba52caf 100644 --- a/tests/functional/lang/eval-okay-flake-ref-to-string.nix +++ b/tests/functional/lang/eval-okay-flake-ref-to-string.nix @@ -1,7 +1,7 @@ builtins.flakeRefToString { - type = "github"; + type = "github"; owner = "NixOS"; - repo = "nixpkgs"; - ref = "23.05"; - dir = "lib"; + repo = "nixpkgs"; + ref = "23.05"; + dir = "lib"; } diff --git a/tests/functional/lang/eval-okay-flatten.nix b/tests/functional/lang/eval-okay-flatten.nix index fe911e9683e..ade74c8e8fe 100644 --- a/tests/functional/lang/eval-okay-flatten.nix +++ b/tests/functional/lang/eval-okay-flatten.nix @@ -2,7 +2,19 @@ with import ./lib.nix; let { - l = ["1" "2" ["3" ["4"] ["5" "6"]] "7"]; + l = [ + "1" + "2" + [ + "3" + [ "4" ] + [ + "5" + "6" + ] + ] + "7" + ]; body = concat (flatten l); } diff --git a/tests/functional/lang/eval-okay-floor-ceil.nix b/tests/functional/lang/eval-okay-floor-ceil.nix index d76a0d86ea7..06f1a13d252 100644 --- a/tests/functional/lang/eval-okay-floor-ceil.nix +++ b/tests/functional/lang/eval-okay-floor-ceil.nix @@ -6,4 +6,11 @@ let n3 = builtins.floor 23; n4 = builtins.ceil 23; in - builtins.concatStringsSep ";" (map toString [ n1 n2 n3 n4 ]) +builtins.concatStringsSep ";" ( + map toString [ + n1 + n2 + n3 + n4 + ] +) diff --git a/tests/functional/lang/eval-okay-foldlStrict-lazy-elements.nix b/tests/functional/lang/eval-okay-foldlStrict-lazy-elements.nix index c666e07f3ae..49751c759d0 100644 --- a/tests/functional/lang/eval-okay-foldlStrict-lazy-elements.nix +++ b/tests/functional/lang/eval-okay-foldlStrict-lazy-elements.nix @@ -1,9 +1,6 @@ # Tests that the rhs argument of op is not forced unconditionally let - lst = builtins.foldl' - (acc: x: acc ++ [ x ]) - [ ] - [ 42 (throw "this shouldn't be evaluated") ]; + lst = builtins.foldl' (acc: x: acc ++ [ x ]) [ ] [ 42 (throw "this shouldn't be evaluated") ]; in builtins.head lst diff --git a/tests/functional/lang/eval-okay-foldlStrict-lazy-initial-accumulator.nix b/tests/functional/lang/eval-okay-foldlStrict-lazy-initial-accumulator.nix index abcd5366ab8..9cf0ef32c87 100644 --- a/tests/functional/lang/eval-okay-foldlStrict-lazy-initial-accumulator.nix +++ b/tests/functional/lang/eval-okay-foldlStrict-lazy-initial-accumulator.nix @@ -1,6 +1,6 @@ # Checks that the nul value for the accumulator is not forced unconditionally. # Some languages provide a foldl' that is strict in this argument, but Nix does not. -builtins.foldl' - (_: x: x) - (throw "This is never forced") - [ "but the results of applying op are" 42 ] +builtins.foldl' (_: x: x) (throw "This is never forced") [ + "but the results of applying op are" + 42 +] diff --git a/tests/functional/lang/eval-okay-fromjson-escapes.nix b/tests/functional/lang/eval-okay-fromjson-escapes.nix index f0071350773..6330e9c8667 100644 --- a/tests/functional/lang/eval-okay-fromjson-escapes.nix +++ b/tests/functional/lang/eval-okay-fromjson-escapes.nix @@ -1,3 +1,4 @@ # This string contains all supported escapes in a JSON string, per json.org # \b and \f are not supported by Nix -builtins.fromJSON ''"quote \" reverse solidus \\ solidus \/ backspace \b formfeed \f newline \n carriage return \r horizontal tab \t 1 char unicode encoded backspace \u0008 1 char unicode encoded e with accent \u00e9 2 char unicode encoded s with caron \u0161 3 char unicode encoded rightwards arrow \u2192"'' +builtins.fromJSON + ''"quote \" reverse solidus \\ solidus \/ backspace \b formfeed \f newline \n carriage return \r horizontal tab \t 1 char unicode encoded backspace \u0008 1 char unicode encoded e with accent \u00e9 2 char unicode encoded s with caron \u0161 3 char unicode encoded rightwards arrow \u2192"'' diff --git a/tests/functional/lang/eval-okay-fromjson.nix b/tests/functional/lang/eval-okay-fromjson.nix index 4c526b9ae5d..0e8a2351fe8 100644 --- a/tests/functional/lang/eval-okay-fromjson.nix +++ b/tests/functional/lang/eval-okay-fromjson.nix @@ -1,41 +1,55 @@ -builtins.fromJSON - '' - { - "Video": { - "Title": "The Penguin Chronicles", - "Width": 1920, - "Height": 1080, - "EmbeddedData": [3.14159, 23493,null, true ,false, -10], - "Thumb": { - "Url": "http://www.example.com/video/5678931", - "Width": 200, - "Height": 250 - }, - "Animated" : false, - "IDs": [116, 943, 234, 38793, true ,false,null, -100], - "Escapes": "\"\\\/\t\n\r\t", - "Subtitle" : false, - "Latitude": 37.7668, - "Longitude": -122.3959 - } - } - '' -== - { Video = - { Title = "The Penguin Chronicles"; - Width = 1920; - Height = 1080; - EmbeddedData = [ 3.14159 23493 null true false (0-10) ]; - Thumb = - { Url = "http://www.example.com/video/5678931"; - Width = 200; - Height = 250; - }; - Animated = false; - IDs = [ 116 943 234 38793 true false null (0-100) ]; - Escapes = "\"\\\/\t\n\r\t"; # supported in JSON but not Nix: \b\f - Subtitle = false; - Latitude = 37.7668; - Longitude = -122.3959; - }; +builtins.fromJSON '' + { + "Video": { + "Title": "The Penguin Chronicles", + "Width": 1920, + "Height": 1080, + "EmbeddedData": [3.14159, 23493,null, true ,false, -10], + "Thumb": { + "Url": "http://www.example.com/video/5678931", + "Width": 200, + "Height": 250 + }, + "Animated" : false, + "IDs": [116, 943, 234, 38793, true ,false,null, -100], + "Escapes": "\"\\\/\t\n\r\t", + "Subtitle" : false, + "Latitude": 37.7668, + "Longitude": -122.3959 + } } +'' == { + Video = { + Title = "The Penguin Chronicles"; + Width = 1920; + Height = 1080; + EmbeddedData = [ + 3.14159 + 23493 + null + true + false + (0 - 10) + ]; + Thumb = { + Url = "http://www.example.com/video/5678931"; + Width = 200; + Height = 250; + }; + Animated = false; + IDs = [ + 116 + 943 + 234 + 38793 + true + false + null + (0 - 100) + ]; + Escapes = "\"\\\/\t\n\r\t"; # supported in JSON but not Nix: \b\f + Subtitle = false; + Latitude = 37.7668; + Longitude = -122.3959; + }; +} diff --git a/tests/functional/lang/eval-okay-functionargs.nix b/tests/functional/lang/eval-okay-functionargs.nix index 68dca62ee18..7c11f19c235 100644 --- a/tests/functional/lang/eval-okay-functionargs.nix +++ b/tests/functional/lang/eval-okay-functionargs.nix @@ -1,29 +1,74 @@ let - stdenvFun = { }: { name = "stdenv"; }; - stdenv2Fun = { }: { name = "stdenv2"; }; - fetchurlFun = { stdenv }: assert stdenv.name == "stdenv"; { name = "fetchurl"; }; - atermFun = { stdenv, fetchurl }: { name = "aterm-${stdenv.name}"; }; - aterm2Fun = { stdenv, fetchurl }: { name = "aterm2-${stdenv.name}"; }; - nixFun = { stdenv, fetchurl, aterm }: { name = "nix-${stdenv.name}-${aterm.name}"; }; - + stdenvFun = + { }: + { + name = "stdenv"; + }; + stdenv2Fun = + { }: + { + name = "stdenv2"; + }; + fetchurlFun = + { stdenv }: + assert stdenv.name == "stdenv"; + { + name = "fetchurl"; + }; + atermFun = + { stdenv, fetchurl }: + { + name = "aterm-${stdenv.name}"; + }; + aterm2Fun = + { stdenv, fetchurl }: + { + name = "aterm2-${stdenv.name}"; + }; + nixFun = + { + stdenv, + fetchurl, + aterm, + }: + { + name = "nix-${stdenv.name}-${aterm.name}"; + }; + mplayerFun = - { stdenv, fetchurl, enableX11 ? false, xorg ? null, enableFoo ? true, foo ? null }: + { + stdenv, + fetchurl, + enableX11 ? false, + xorg ? null, + enableFoo ? true, + foo ? null, + }: assert stdenv.name == "stdenv2"; assert enableX11 -> xorg.libXv.name == "libXv"; assert enableFoo -> foo != null; - { name = "mplayer-${stdenv.name}.${xorg.libXv.name}-${xorg.libX11.name}"; }; + { + name = "mplayer-${stdenv.name}.${xorg.libXv.name}-${xorg.libX11.name}"; + }; - makeOverridable = f: origArgs: f origArgs // - { override = newArgs: + makeOverridable = + f: origArgs: + f origArgs + // { + override = + newArgs: makeOverridable f (origArgs // (if builtins.isFunction newArgs then newArgs origArgs else newArgs)); }; - - callPackage_ = pkgs: f: args: + + callPackage_ = + pkgs: f: args: makeOverridable f ((builtins.intersectAttrs (builtins.functionArgs f) pkgs) // args); allPackages = - { overrides ? (pkgs: pkgsPrev: { }) }: + { + overrides ? (pkgs: pkgsPrev: { }), + }: let callPackage = callPackage_ pkgs; pkgs = pkgsStd // (overrides pkgs pkgsStd); @@ -34,18 +79,40 @@ let fetchurl = callPackage fetchurlFun { }; aterm = callPackage atermFun { }; xorg = callPackage xorgFun { }; - mplayer = callPackage mplayerFun { stdenv = pkgs.stdenv2; enableFoo = false; }; + mplayer = callPackage mplayerFun { + stdenv = pkgs.stdenv2; + enableFoo = false; + }; nix = callPackage nixFun { }; }; - in pkgs; + in + pkgs; + + libX11Fun = + { stdenv, fetchurl }: + { + name = "libX11"; + }; + libX11_2Fun = + { stdenv, fetchurl }: + { + name = "libX11_2"; + }; + libXvFun = + { + stdenv, + fetchurl, + libX11, + }: + { + name = "libXv"; + }; - libX11Fun = { stdenv, fetchurl }: { name = "libX11"; }; - libX11_2Fun = { stdenv, fetchurl }: { name = "libX11_2"; }; - libXvFun = { stdenv, fetchurl, libX11 }: { name = "libXv"; }; - xorgFun = { pkgs }: - let callPackage = callPackage_ (pkgs // pkgs.xorg); in + let + callPackage = callPackage_ (pkgs // pkgs.xorg); + in { libX11 = callPackage libX11Fun { }; libXv = callPackage libXvFun { }; @@ -56,25 +123,28 @@ in let pkgs = allPackages { }; - + pkgs2 = allPackages { overrides = pkgs: pkgsPrev: { stdenv = pkgs.stdenv2; nix = pkgsPrev.nix.override { aterm = aterm2Fun { inherit (pkgs) stdenv fetchurl; }; }; - xorg = pkgsPrev.xorg // { libX11 = libX11_2Fun { inherit (pkgs) stdenv fetchurl; }; }; + xorg = pkgsPrev.xorg // { + libX11 = libX11_2Fun { inherit (pkgs) stdenv fetchurl; }; + }; }; }; - + in - [ pkgs.stdenv.name - pkgs.fetchurl.name - pkgs.aterm.name - pkgs2.aterm.name - pkgs.xorg.libX11.name - pkgs.xorg.libXv.name - pkgs.mplayer.name - pkgs2.mplayer.name - pkgs.nix.name - pkgs2.nix.name - ] +[ + pkgs.stdenv.name + pkgs.fetchurl.name + pkgs.aterm.name + pkgs2.aterm.name + pkgs.xorg.libX11.name + pkgs.xorg.libXv.name + pkgs.mplayer.name + pkgs2.mplayer.name + pkgs.nix.name + pkgs2.nix.name +] diff --git a/tests/functional/lang/eval-okay-getattrpos-functionargs.nix b/tests/functional/lang/eval-okay-getattrpos-functionargs.nix index 11d6bb0e3ac..9692911cfc9 100644 --- a/tests/functional/lang/eval-okay-getattrpos-functionargs.nix +++ b/tests/functional/lang/eval-okay-getattrpos-functionargs.nix @@ -1,4 +1,8 @@ let - fun = { foo }: {}; + fun = { foo }: { }; pos = builtins.unsafeGetAttrPos "foo" (builtins.functionArgs fun); -in { inherit (pos) column line; file = baseNameOf pos.file; } +in +{ + inherit (pos) column line; + file = baseNameOf pos.file; +} diff --git a/tests/functional/lang/eval-okay-getattrpos.nix b/tests/functional/lang/eval-okay-getattrpos.nix index ca6b0796154..25bc57444fa 100644 --- a/tests/functional/lang/eval-okay-getattrpos.nix +++ b/tests/functional/lang/eval-okay-getattrpos.nix @@ -3,4 +3,8 @@ let foo = "bar"; }; pos = builtins.unsafeGetAttrPos "foo" as; -in { inherit (pos) column line; file = baseNameOf pos.file; } +in +{ + inherit (pos) column line; + file = baseNameOf pos.file; +} diff --git a/tests/functional/lang/eval-okay-groupBy.nix b/tests/functional/lang/eval-okay-groupBy.nix index 862d89dbd67..f4de5444a3c 100644 --- a/tests/functional/lang/eval-okay-groupBy.nix +++ b/tests/functional/lang/eval-okay-groupBy.nix @@ -1,5 +1,5 @@ with import ./lib.nix; -builtins.groupBy (n: - builtins.substring 0 1 (builtins.hashString "sha256" (toString n)) -) (range 0 31) +builtins.groupBy (n: builtins.substring 0 1 (builtins.hashString "sha256" (toString n))) ( + range 0 31 +) diff --git a/tests/functional/lang/eval-okay-hashfile.nix b/tests/functional/lang/eval-okay-hashfile.nix index aff5a185681..aeaf09f43f6 100644 --- a/tests/functional/lang/eval-okay-hashfile.nix +++ b/tests/functional/lang/eval-okay-hashfile.nix @@ -1,4 +1,14 @@ let - paths = [ ./data ./binary-data ]; + paths = [ + ./data + ./binary-data + ]; in - builtins.concatLists (map (hash: map (builtins.hashFile hash) paths) ["md5" "sha1" "sha256" "sha512"]) +builtins.concatLists ( + map (hash: map (builtins.hashFile hash) paths) [ + "md5" + "sha1" + "sha256" + "sha512" + ] +) diff --git a/tests/functional/lang/eval-okay-hashstring.nix b/tests/functional/lang/eval-okay-hashstring.nix index b0f62b245ca..c760b00435e 100644 --- a/tests/functional/lang/eval-okay-hashstring.nix +++ b/tests/functional/lang/eval-okay-hashstring.nix @@ -1,4 +1,15 @@ let - strings = [ "" "text 1" "text 2" ]; + strings = [ + "" + "text 1" + "text 2" + ]; in - builtins.concatLists (map (hash: map (builtins.hashString hash) strings) ["md5" "sha1" "sha256" "sha512"]) +builtins.concatLists ( + map (hash: map (builtins.hashString hash) strings) [ + "md5" + "sha1" + "sha256" + "sha512" + ] +) diff --git a/tests/functional/lang/eval-okay-if.nix b/tests/functional/lang/eval-okay-if.nix index 23e4c74d501..66b9d15b8cc 100644 --- a/tests/functional/lang/eval-okay-if.nix +++ b/tests/functional/lang/eval-okay-if.nix @@ -1 +1,6 @@ -if "foo" != "f" + "oo" then 1 else if false then 2 else 3 +if "foo" != "f" + "oo" then + 1 +else if false then + 2 +else + 3 diff --git a/tests/functional/lang/eval-okay-import.nix b/tests/functional/lang/eval-okay-import.nix index 0b18d941312..484dccac0e1 100644 --- a/tests/functional/lang/eval-okay-import.nix +++ b/tests/functional/lang/eval-okay-import.nix @@ -8,4 +8,5 @@ let builtins = builtins // overrides; } // import ./lib.nix; -in scopedImport overrides ./imported.nix +in +scopedImport overrides ./imported.nix diff --git a/tests/functional/lang/eval-okay-inherit-attr-pos.nix b/tests/functional/lang/eval-okay-inherit-attr-pos.nix index 017ab1d364d..c162d119677 100644 --- a/tests/functional/lang/eval-okay-inherit-attr-pos.nix +++ b/tests/functional/lang/eval-okay-inherit-attr-pos.nix @@ -4,9 +4,9 @@ let y = { inherit d x; }; z = { inherit (y) d x; }; in - [ - (builtins.unsafeGetAttrPos "d" y) - (builtins.unsafeGetAttrPos "x" y) - (builtins.unsafeGetAttrPos "d" z) - (builtins.unsafeGetAttrPos "x" z) - ] +[ + (builtins.unsafeGetAttrPos "d" y) + (builtins.unsafeGetAttrPos "x" y) + (builtins.unsafeGetAttrPos "d" z) + (builtins.unsafeGetAttrPos "x" z) +] diff --git a/tests/functional/lang/eval-okay-inherit-from.nix b/tests/functional/lang/eval-okay-inherit-from.nix index b72a1c639fd..1a0980aafb1 100644 --- a/tests/functional/lang/eval-okay-inherit-from.nix +++ b/tests/functional/lang/eval-okay-inherit-from.nix @@ -1,5 +1,12 @@ let - inherit (builtins.trace "used" { a = 1; b = 2; }) a b; + inherit + (builtins.trace "used" { + a = 1; + b = 2; + }) + a + b + ; x.c = 3; y.d = 4; @@ -13,4 +20,14 @@ let }; }; in - [ a b rec { x.c = []; inherit (x) c; inherit (y) d; __overrides.y.d = []; } merged ] +[ + a + b + rec { + x.c = [ ]; + inherit (x) c; + inherit (y) d; + __overrides.y.d = [ ]; + } + merged +] diff --git a/tests/functional/lang/eval-okay-intersectAttrs.nix b/tests/functional/lang/eval-okay-intersectAttrs.nix index 39d49938cc2..bf4d58a9969 100644 --- a/tests/functional/lang/eval-okay-intersectAttrs.nix +++ b/tests/functional/lang/eval-okay-intersectAttrs.nix @@ -1,6 +1,6 @@ let - alphabet = - { a = "a"; + alphabet = { + a = "a"; b = "b"; c = "c"; d = "d"; @@ -28,23 +28,46 @@ let z = "z"; }; foo = { - inherit (alphabet) f o b a r z q u x; + inherit (alphabet) + f + o + b + a + r + z + q + u + x + ; aa = throw "aa"; }; alphabetFail = builtins.mapAttrs throw alphabet; in -[ (builtins.intersectAttrs { a = abort "l1"; } { b = abort "r1"; }) +[ + (builtins.intersectAttrs { a = abort "l1"; } { b = abort "r1"; }) (builtins.intersectAttrs { a = abort "l2"; } { a = 1; }) (builtins.intersectAttrs alphabetFail { a = 1; }) - (builtins.intersectAttrs { a = abort "laa"; } alphabet) + (builtins.intersectAttrs { a = abort "laa"; } alphabet) (builtins.intersectAttrs alphabetFail { m = 1; }) - (builtins.intersectAttrs { m = abort "lam"; } alphabet) + (builtins.intersectAttrs { m = abort "lam"; } alphabet) (builtins.intersectAttrs alphabetFail { n = 1; }) - (builtins.intersectAttrs { n = abort "lan"; } alphabet) - (builtins.intersectAttrs alphabetFail { n = 1; p = 2; }) - (builtins.intersectAttrs { n = abort "lan2"; p = abort "lap"; } alphabet) - (builtins.intersectAttrs alphabetFail { n = 1; p = 2; }) - (builtins.intersectAttrs { n = abort "lan2"; p = abort "lap"; } alphabet) + (builtins.intersectAttrs { n = abort "lan"; } alphabet) + (builtins.intersectAttrs alphabetFail { + n = 1; + p = 2; + }) + (builtins.intersectAttrs { + n = abort "lan2"; + p = abort "lap"; + } alphabet) + (builtins.intersectAttrs alphabetFail { + n = 1; + p = 2; + }) + (builtins.intersectAttrs { + n = abort "lan2"; + p = abort "lap"; + } alphabet) (builtins.intersectAttrs alphabetFail alphabet) (builtins.intersectAttrs alphabet foo == builtins.intersectAttrs foo alphabet) ] diff --git a/tests/functional/lang/eval-okay-list.nix b/tests/functional/lang/eval-okay-list.nix index d433bcf908b..b5045a75378 100644 --- a/tests/functional/lang/eval-okay-list.nix +++ b/tests/functional/lang/eval-okay-list.nix @@ -2,6 +2,11 @@ with import ./lib.nix; let { - body = concat ["foo" "bar" "bla" "test"]; - -} \ No newline at end of file + body = concat [ + "foo" + "bar" + "bla" + "test" + ]; + +} diff --git a/tests/functional/lang/eval-okay-listtoattrs.nix b/tests/functional/lang/eval-okay-listtoattrs.nix index 4186e029b53..1de9d6d62f5 100644 --- a/tests/functional/lang/eval-okay-listtoattrs.nix +++ b/tests/functional/lang/eval-okay-listtoattrs.nix @@ -1,11 +1,24 @@ # this test shows how to use listToAttrs and that evaluation is still lazy (throw isn't called) with import ./lib.nix; -let - asi = name: value : { inherit name value; }; - list = [ ( asi "a" "A" ) ( asi "b" "B" ) ]; +let + asi = name: value: { inherit name value; }; + list = [ + (asi "a" "A") + (asi "b" "B") + ]; a = builtins.listToAttrs list; - b = builtins.listToAttrs ( list ++ list ); - r = builtins.listToAttrs [ (asi "result" [ a b ]) ( asi "throw" (throw "this should not be thrown")) ]; - x = builtins.listToAttrs [ (asi "foo" "bar") (asi "foo" "bla") ]; -in concat (map (x: x.a) r.result) + x.foo + b = builtins.listToAttrs (list ++ list); + r = builtins.listToAttrs [ + (asi "result" [ + a + b + ]) + (asi "throw" (throw "this should not be thrown")) + ]; + x = builtins.listToAttrs [ + (asi "foo" "bar") + (asi "foo" "bla") + ]; +in +concat (map (x: x.a) r.result) + x.foo diff --git a/tests/functional/lang/eval-okay-logic.nix b/tests/functional/lang/eval-okay-logic.nix index fbb12794401..55cd2fc00fd 100644 --- a/tests/functional/lang/eval-okay-logic.nix +++ b/tests/functional/lang/eval-okay-logic.nix @@ -1 +1,2 @@ -assert !false && (true || false) -> true; 1 +assert !false && (true || false) -> true; +1 diff --git a/tests/functional/lang/eval-okay-map.nix b/tests/functional/lang/eval-okay-map.nix index a76c1d81145..22059f37a57 100644 --- a/tests/functional/lang/eval-okay-map.nix +++ b/tests/functional/lang/eval-okay-map.nix @@ -1,3 +1,9 @@ with import ./lib.nix; -concat (map (x: x + "bar") [ "foo" "bla" "xyzzy" ]) \ No newline at end of file +concat ( + map (x: x + "bar") [ + "foo" + "bla" + "xyzzy" + ] +) diff --git a/tests/functional/lang/eval-okay-mapattrs.nix b/tests/functional/lang/eval-okay-mapattrs.nix index f075b6275e5..c1182d13db5 100644 --- a/tests/functional/lang/eval-okay-mapattrs.nix +++ b/tests/functional/lang/eval-okay-mapattrs.nix @@ -1,3 +1,6 @@ with import ./lib.nix; -builtins.mapAttrs (name: value: name + "-" + value) { x = "foo"; y = "bar"; } +builtins.mapAttrs (name: value: name + "-" + value) { + x = "foo"; + y = "bar"; +} diff --git a/tests/functional/lang/eval-okay-merge-dynamic-attrs.nix b/tests/functional/lang/eval-okay-merge-dynamic-attrs.nix index f459a554f34..8ee8e503a6a 100644 --- a/tests/functional/lang/eval-okay-merge-dynamic-attrs.nix +++ b/tests/functional/lang/eval-okay-merge-dynamic-attrs.nix @@ -1,9 +1,17 @@ { - set1 = { a = 1; }; - set1 = { "${"b" + ""}" = 2; }; + set1 = { + a = 1; + }; + set1 = { + "${"b" + ""}" = 2; + }; - set2 = { "${"b" + ""}" = 2; }; - set2 = { a = 1; }; + set2 = { + "${"b" + ""}" = 2; + }; + set2 = { + a = 1; + }; set3.a = 1; set3."${"b" + ""}" = 2; diff --git a/tests/functional/lang/eval-okay-nested-with.nix b/tests/functional/lang/eval-okay-nested-with.nix index ba9d79aa79b..ee069eaa1c2 100644 --- a/tests/functional/lang/eval-okay-nested-with.nix +++ b/tests/functional/lang/eval-okay-nested-with.nix @@ -1,3 +1 @@ -with { x = 1; }; -with { x = 2; }; -x +with { x = 1; }; with { x = 2; }; x diff --git a/tests/functional/lang/eval-okay-new-let.nix b/tests/functional/lang/eval-okay-new-let.nix index 73812314150..1a938ce718f 100644 --- a/tests/functional/lang/eval-okay-new-let.nix +++ b/tests/functional/lang/eval-okay-new-let.nix @@ -1,14 +1,16 @@ let - f = z: + f = + z: let x = "foo"; y = "bar"; body = 1; # compat test in - z + x + y; + z + x + y; arg = "xyzzy"; -in f arg +in +f arg diff --git a/tests/functional/lang/eval-okay-null-dynamic-attrs.nix b/tests/functional/lang/eval-okay-null-dynamic-attrs.nix index b060c0bc985..76286b6225c 100644 --- a/tests/functional/lang/eval-okay-null-dynamic-attrs.nix +++ b/tests/functional/lang/eval-okay-null-dynamic-attrs.nix @@ -1 +1 @@ -{ ${null} = true; } == {} +{ ${null} = true; } == { } diff --git a/tests/functional/lang/eval-okay-overrides.nix b/tests/functional/lang/eval-okay-overrides.nix index 719bdc9c05e..1c0d5d7c2ea 100644 --- a/tests/functional/lang/eval-okay-overrides.nix +++ b/tests/functional/lang/eval-okay-overrides.nix @@ -1,8 +1,12 @@ let - overrides = { a = 2; b = 3; }; + overrides = { + a = 2; + b = 3; + }; -in (rec { +in +(rec { __overrides = overrides; x = a; a = 1; diff --git a/tests/functional/lang/eval-okay-parse-flake-ref.nix b/tests/functional/lang/eval-okay-parse-flake-ref.nix index db4ed2742cd..404c5df0824 100644 --- a/tests/functional/lang/eval-okay-parse-flake-ref.nix +++ b/tests/functional/lang/eval-okay-parse-flake-ref.nix @@ -1 +1 @@ - builtins.parseFlakeRef "github:NixOS/nixpkgs/23.05?dir=lib" +builtins.parseFlakeRef "github:NixOS/nixpkgs/23.05?dir=lib" diff --git a/tests/functional/lang/eval-okay-partition.nix b/tests/functional/lang/eval-okay-partition.nix index 846d2ce4948..b9566edf979 100644 --- a/tests/functional/lang/eval-okay-partition.nix +++ b/tests/functional/lang/eval-okay-partition.nix @@ -1,5 +1,8 @@ with import ./lib.nix; -builtins.partition - (x: x / 2 * 2 == x) - (builtins.concatLists [ (range 0 10) (range 100 110) ]) +builtins.partition (x: x / 2 * 2 == x) ( + builtins.concatLists [ + (range 0 10) + (range 100 110) + ] +) diff --git a/tests/functional/lang/eval-okay-path.nix b/tests/functional/lang/eval-okay-path.nix index 599b3354147..b8b48aae1a6 100644 --- a/tests/functional/lang/eval-okay-path.nix +++ b/tests/functional/lang/eval-okay-path.nix @@ -1,15 +1,15 @@ [ - (builtins.path - { path = ./.; - filter = path: _: baseNameOf path == "data"; - recursive = true; - sha256 = "1yhm3gwvg5a41yylymgblsclk95fs6jy72w0wv925mmidlhcq4sw"; - name = "output"; - }) - (builtins.path - { path = ./data; - recursive = false; - sha256 = "0k4lwj58f2w5yh92ilrwy9917pycipbrdrr13vbb3yd02j09vfxm"; - name = "output"; - }) + (builtins.path { + path = ./.; + filter = path: _: baseNameOf path == "data"; + recursive = true; + sha256 = "1yhm3gwvg5a41yylymgblsclk95fs6jy72w0wv925mmidlhcq4sw"; + name = "output"; + }) + (builtins.path { + path = ./data; + recursive = false; + sha256 = "0k4lwj58f2w5yh92ilrwy9917pycipbrdrr13vbb3yd02j09vfxm"; + name = "output"; + }) ] diff --git a/tests/functional/lang/eval-okay-patterns.nix b/tests/functional/lang/eval-okay-patterns.nix index 96fd25a0151..b92b232d2fa 100644 --- a/tests/functional/lang/eval-okay-patterns.nix +++ b/tests/functional/lang/eval-okay-patterns.nix @@ -1,16 +1,59 @@ let - f = args@{x, y, z}: x + args.y + z; + f = + args@{ + x, + y, + z, + }: + x + args.y + z; - g = {x, y, z}@args: f args; + g = + { + x, + y, + z, + }@args: + f args; - h = {x ? "d", y ? x, z ? args.x}@args: x + y + z; + h = + { + x ? "d", + y ? x, + z ? args.x, + }@args: + x + y + z; - j = {x, y, z, ...}: x + y + z; + j = + { + x, + y, + z, + ... + }: + x + y + z; in - f {x = "a"; y = "b"; z = "c";} + - g {x = "x"; y = "y"; z = "z";} + - h {x = "D";} + - h {x = "D"; y = "E"; z = "F";} + - j {x = "i"; y = "j"; z = "k"; bla = "bla"; foo = "bar";} +f { + x = "a"; + y = "b"; + z = "c"; +} ++ g { + x = "x"; + y = "y"; + z = "z"; +} ++ h { x = "D"; } ++ h { + x = "D"; + y = "E"; + z = "F"; +} ++ j { + x = "i"; + y = "j"; + z = "k"; + bla = "bla"; + foo = "bar"; +} diff --git a/tests/functional/lang/eval-okay-print.nix b/tests/functional/lang/eval-okay-print.nix index d36ba4da31c..1ad46560235 100644 --- a/tests/functional/lang/eval-okay-print.nix +++ b/tests/functional/lang/eval-okay-print.nix @@ -1 +1,15 @@ -with builtins; trace [(1+1)] [ null toString (deepSeq "x") (a: a) (let x=[x]; in x) ] +with builtins; +trace + [ (1 + 1) ] + [ + null + toString + (deepSeq "x") + (a: a) + ( + let + x = [ x ]; + in + x + ) + ] diff --git a/tests/functional/lang/eval-okay-readFileType.nix b/tests/functional/lang/eval-okay-readFileType.nix index 174fb6c3a02..79beb9a6e25 100644 --- a/tests/functional/lang/eval-okay-readFileType.nix +++ b/tests/functional/lang/eval-okay-readFileType.nix @@ -1,6 +1,6 @@ { - bar = builtins.readFileType ./readDir/bar; - foo = builtins.readFileType ./readDir/foo; + bar = builtins.readFileType ./readDir/bar; + foo = builtins.readFileType ./readDir/foo; linked = builtins.readFileType ./readDir/linked; - ldir = builtins.readFileType ./readDir/ldir; + ldir = builtins.readFileType ./readDir/ldir; } diff --git a/tests/functional/lang/eval-okay-redefine-builtin.nix b/tests/functional/lang/eval-okay-redefine-builtin.nix index df9fc3f37d2..ec95ffa932a 100644 --- a/tests/functional/lang/eval-okay-redefine-builtin.nix +++ b/tests/functional/lang/eval-okay-redefine-builtin.nix @@ -1,3 +1,4 @@ let throw = abort "Error!"; -in (builtins.tryEval ).success +in +(builtins.tryEval ).success diff --git a/tests/functional/lang/eval-okay-regex-match.nix b/tests/functional/lang/eval-okay-regex-match.nix index 273e2590713..54b995996f1 100644 --- a/tests/functional/lang/eval-okay-regex-match.nix +++ b/tests/functional/lang/eval-okay-regex-match.nix @@ -8,22 +8,34 @@ let in -assert matches "foobar" "foobar"; -assert matches "fo*" "f"; +assert matches "foobar" "foobar"; +assert matches "fo*" "f"; assert !matches "fo+" "f"; -assert matches "fo*" "fo"; -assert matches "fo*" "foo"; -assert matches "fo+" "foo"; -assert matches "fo{1,2}" "foo"; +assert matches "fo*" "fo"; +assert matches "fo*" "foo"; +assert matches "fo+" "foo"; +assert matches "fo{1,2}" "foo"; assert !matches "fo{1,2}" "fooo"; assert !matches "fo*" "foobar"; -assert matches "[[:space:]]+([^[:space:]]+)[[:space:]]+" " foo "; +assert matches "[[:space:]]+([^[:space:]]+)[[:space:]]+" " foo "; assert !matches "[[:space:]]+([[:upper:]]+)[[:space:]]+" " foo "; assert match "(.*)\\.nix" "foobar.nix" == [ "foobar" ]; assert match "[[:space:]]+([[:upper:]]+)[[:space:]]+" " FOO " == [ "FOO" ]; -assert splitFN "/path/to/foobar.nix" == [ "/path/to/" "/path/to" "foobar" "nix" ]; -assert splitFN "foobar.cc" == [ null null "foobar" "cc" ]; +assert + splitFN "/path/to/foobar.nix" == [ + "/path/to/" + "/path/to" + "foobar" + "nix" + ]; +assert + splitFN "foobar.cc" == [ + null + null + "foobar" + "cc" + ]; true diff --git a/tests/functional/lang/eval-okay-regex-match2.exp b/tests/functional/lang/eval-okay-regex-match2.exp new file mode 100644 index 00000000000..b7fb4e05e16 --- /dev/null +++ b/tests/functional/lang/eval-okay-regex-match2.exp @@ -0,0 +1 @@ +[ null null null null null null null null null null [ ] [ ] null null null null [ "gnu" "m4/m4-1.4.19.tar.bz2" ] null [ "cpan" "src/5.0/perl-5.40.0.tar.gz" ] null null null [ "10" "" ] [ "11" "" ] [ "36" ] null [ "exec" ] [ ] null [ "26" ] null [ "26" ] null [ ] null null null null null [ "meson.patch?h=mingw-w64-xorgproto&id=7b817efc3144a50e6766817c4ca7242f8ce49307" ] null null [ "xmlto" ] null null [ "exec" ] null null [ ] [ ] null null [ "coconutbattery-4.0.2,152" ] [ "12" "0" ] [ "12" "8" ] [ "8" "9" "5" "30" ] [ "9" "7" "1" "26" ] null null [ ] [ ] null [ ] null [ ] null null [ null null "draupnir" ] [ ] [ ] null null [ null null "renderer" ] [ ] [ ] [ null ] null [ null ] null null null [ ] [ ] [ "p" ] [ "p" ] [ "systemtap" ] null [ ] null null [ ] null [ "20220722-71c783507536-b7eae18423ef" ] [ "20220726-bac6d66b5ca1-5b966f2f136c" ] [ ] [ "0.3.2308" ] null null null [ "17.0.14+" "7" ] null [ null ] [ null ] null null [ "21.0.7+" "6" ] null null [ ] [ ] [ "8u442" "06" ] [ ] [ "jna" "5.6.0" null null ] [ "jna" "5.6.0" null null ] [ ] [ ] null [ "2" ] null [ ] [ ] null null [ ] [ ] null null [ ] null [ ] [ "https://github.com/GRA0007/google-cloud-rs.git" null null null "4a2db92efd57a896e14d18877458c6ae43418aec" ] [ "https://github.com/GRA0007/google-cloud-rs.git" null null null "4a2db92efd57a896e14d18877458c6ae43418aec" ] null [ ] null [ "rejeep" "ansi.el" ] null [ "rejeep" "commander.el" ] null [ "2.2.4" "20231021.200112" "6" ] [ "2.2.4" "20231021.200112" "6" ] [ ] [ ] null null [ "" ] [ "" ".git" ] [ "" "\\.git" ] null null null [ "" "__pycache__" ] [ "" "__pycache__" ] null null null [ "" ] null null [ ] [ ] [ ] [ "8u442" "06" ] [ ] [ ] [ "simulator" ] null null null null null null [ "notify-send" ] [ "playlistmanager" ] [ ] [ ] null null null null [ "name" ] [ "name" ] null null null null [ "pypy" "27" ] [ "pypy" "310" ] [ "refs/heads/master" ] null [ "refs/heads/master" ] null null [ ] null null null null [ ] [ ] [ ] [ ] [ "b7eae18423ef" ] [ "20220726-bac6d66b5ca1-5b966f2f136c" ] null null [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ "" ] [ ] ] diff --git a/tests/functional/lang/eval-okay-regex-match2.nix b/tests/functional/lang/eval-okay-regex-match2.nix new file mode 100644 index 00000000000..31a94423d86 --- /dev/null +++ b/tests/functional/lang/eval-okay-regex-match2.nix @@ -0,0 +1,938 @@ +# Derived from nixpkgs f870c6ccc8951fc48aeb293cf3e98ade6ac42668 by instrumenting +# builtins.match to collect at most 2 non-matching and 2 matching cases of every +# regex used when running: +# `nix-env --query --available --out-path --eval-system x86_64-linux`. + +builtins.map + ( + list: + let + re = builtins.head list; + str = builtins.elemAt list 1; + in + builtins.match re str + ) + [ + [ + ''(.*)e?abi.*'' + ''linux'' + ] + [ + ''(.*)e?abi.*'' + ''linux'' + ] + [ + ''.*-none.*'' + ''x86_64-unknown-linux-gnu'' + ] + [ + ''.*nvptx.*'' + ''x86_64-unknown-linux-gnu'' + ] + [ + ''.*switch.*'' + ''x86_64-unknown-linux-gnu'' + ] + [ + ''.*-uefi.*'' + ''x86_64-unknown-linux-gnu'' + ] + [ + ''.*-none.*'' + ''x86_64-unknown-linux-gnu'' + ] + [ + ''.*nvptx.*'' + ''x86_64-unknown-linux-gnu'' + ] + [ + ''.*switch.*'' + ''x86_64-unknown-linux-gnu'' + ] + [ + ''.*-uefi.*'' + ''x86_64-unknown-linux-gnu'' + ] + [ + ''[[:alnum:]+_?=-][[:alnum:]+._?=-]*'' + ''bootstrap-stage0-glibc-bootstrapFiles'' + ] + [ + ''[[:alnum:]+_?=-][[:alnum:]+._?=-]*'' + ''glibc-2.40-66'' + ] + [ + ''mirror://([a-z]+)/(.*)'' + ''https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.gz'' + ] + [ + ''mirror://([a-z]+)/(.*)'' + ''https://git.savannah.gnu.org/cgit/config.git/plain/config.guess?id=948ae97ca5703224bd3eada06b7a69f40dd15a02'' + ] + [ + ''.*/.*'' + ''mktemp'' + ] + [ + ''.*/.*'' + ''rm'' + ] + [ + ''mirror://([a-z]+)/(.*)'' + ''mirror://gnu/m4/m4-1.4.19.tar.bz2'' + ] + [ + ''5\.[0-9]*[13579]\..+'' + ''5.40.0'' + ] + [ + ''mirror://([a-z]+)/(.*)'' + ''mirror://cpan/src/5.0/perl-5.40.0.tar.gz'' + ] + [ + ''5\.[0-9]*[13579]\..+'' + ''5.40.0'' + ] + [ + ''^([0-9][0-9\.]*)(.*)$'' + ''addons'' + ] + [ + ''^([0-9][0-9\.]*)(.*)$'' + ''extras'' + ] + [ + ''^([0-9][0-9\.]*)(.*)$'' + ''10'' + ] + [ + ''^([0-9][0-9\.]*)(.*)$'' + ''11'' + ] + [ + ''[[:space:]]*0*(-?[[:digit:]]+)[[:space:]]*'' + ''36'' + ] + [ + ''0+'' + ''36'' + ] + [ + ''/bin/([^/]+)'' + ''/bin/exec'' + ] + [ + ''[[:alnum:],._+:@%/-]+'' + ''/bin/exec'' + ] + [ + ''[[:alnum:],._+:@%/-]+'' + '''' + ] + [ + ''[[:space:]]*(-?[[:digit:]]+)[[:space:]]*'' + ''26'' + ] + [ + ''0[[:digit:]]+'' + ''26'' + ] + [ + ''[[:space:]]*(-?[[:digit:]]+)[[:space:]]*'' + ''26'' + ] + [ + ''0[[:digit:]]+'' + ''26'' + ] + [ + ''[[:alnum:],._+:@%/-]+'' + ''@tcl@'' + ] + [ + ''[[:alnum:],._+:@%/-]+'' + ''@[a-zA-Z_][0-9A-Za-z_'-]*@'' + ] + [ + ''.*pypy.*'' + ''/nix/store/8w718rm43x7z73xhw9d6vh8s4snrq67h-python3-3.12.10/bin/python3.12'' + ] + [ + ''(.*/)?\.\.(/.*)?'' + ''package.nix'' + ] + [ + ''/bin/([^/]+)'' + '''' + ] + [ + ''[[:alnum:]+_?=-][[:alnum:]+._?=-]*'' + ''meson.patch?h=mingw-w64-xorgproto&id=7b817efc3144a50e6766817c4ca7242f8ce49307'' + ] + [ + ''\.*(.*)'' + ''meson.patch?h=mingw-w64-xorgproto&id=7b817efc3144a50e6766817c4ca7242f8ce49307'' + ] + [ + ''/bin/([^/]+)'' + '''' + ] + [ + ''.*-rc.*'' + ''2.49.0'' + ] + [ + ''(.*)\.git'' + ''xmlto.git'' + ] + [ + ''[a-f0-9]*'' + ''0.0.29'' + ] + [ + ''.*-rc.*'' + ''2.49.0'' + ] + [ + ''/bin/([^/]+)'' + ''/bin/exec'' + ] + [ + ''.*-polly.*'' + ''/nix/store/0yxfdnfxbzczjxhgdpac81jnas194wfj-gnu-install-dirs.patch'' + ] + [ + ''.*-polly.*'' + ''/nix/store/jh2pda7psaasq85b2rrigmkjdbl8d0a1-llvm-lit-cfg-add-libs-to-dylib-path.patch'' + ] + [ + ''.*-polly.*'' + ''/nix/store/x868j4ih7wqiivf6wr9m4g424jav0hpq-gnu-install-dirs-polly.patch'' + ] + [ + ''.*-polly.*'' + ''/nix/store/gr73nf6sca9nyzl88x58y3qxrav04yhd-polly-lit-cfg-add-libs-to-dylib-path.patch'' + ] + [ + ''(.*/)?\.\.(/.*)?'' + ''package.nix'' + ] + [ + ''[[:alnum:]+_?=-][[:alnum:]+._?=-]*'' + ''coconutbattery-4.0.2,152'' + ] + [ + ''\.*(.*)'' + ''coconutbattery-4.0.2,152'' + ] + [ + ''^([[:digit:]]+)\.([[:digit:]]+)$'' + ''12.0'' + ] + [ + ''^([[:digit:]]+)\.([[:digit:]]+)$'' + ''12.8'' + ] + [ + ''^([[:digit:]]+)\.([[:digit:]]+)\.([[:digit:]]+)\.([[:digit:]]+)$'' + ''8.9.5.30'' + ] + [ + ''^([[:digit:]]+)\.([[:digit:]]+)\.([[:digit:]]+)\.([[:digit:]]+)$'' + ''9.7.1.26'' + ] + [ + ''^/.*'' + ''8.20'' + ] + [ + ''^/.*'' + ''8.20'' + ] + [ + ''^github.*'' + ''github.com'' + ] + [ + ''^github.*'' + ''github.com'' + ] + [ + ''^github.*'' + ''gitlab.inria.fr'' + ] + [ + ''^gitlab.*'' + ''gitlab.inria.fr'' + ] + [ + ''^github.*'' + ''gitlab.inria.fr'' + ] + [ + ''^gitlab.*'' + ''gitlab.inria.fr'' + ] + [ + ''^gitlab.*'' + ''sf.snu.ac.kr'' + ] + [ + ''^gitlab.*'' + ''sf.snu.ac.kr'' + ] + [ + ''^(@([^/]+)/)?([^/]+)$'' + ''draupnir'' + ] + [ + ''^[[:digit:]].*'' + ''0xproto'' + ] + [ + ''^[[:digit:]].*'' + ''3270'' + ] + [ + ''^[[:digit:]].*'' + ''adwaita-mono'' + ] + [ + ''^[[:digit:]].*'' + ''agave'' + ] + [ + ''^(@([^/]+)/)?([^/]+)$'' + ''renderer'' + ] + [ + ''[^[:space:]]*'' + ''900,906,908,1010,1012,1030'' + ] + [ + ''[^[:space:]]*'' + '''' + ] + [ + ''.*[0-9]_LIN(UX)?.sh'' + ''Wolfram_14.2.1_LIN.sh'' + ] + [ + ''.*[0-9]_LIN(UX)?.sh'' + ''Wolfram_14.2.1_LIN_Bndl.sh'' + ] + [ + ''.*[0-9]_LIN(UX)?.sh'' + ''Wolfram_14.2.0_LIN.sh'' + ] + [ + ''.*[0-9]_LIN(UX)?.sh'' + ''Wolfram_14.2.0_LIN_Bndl.sh'' + ] + [ + ''[A-Z]'' + ''b'' + ] + [ + ''[A-Z]'' + ''l'' + ] + [ + ''[A-Z]'' + ''E'' + ] + [ + ''[A-Z]'' + ''T'' + ] + [ + ''([0-9A-Za-z._])[0-9A-Za-z._-]*'' + ''pythoncheck.sh'' + ] + [ + ''([0-9A-Za-z._])[0-9A-Za-z._-]*'' + ''pythoncheck.sh'' + ] + [ + ''(.*)\.git'' + ''systemtap.git'' + ] + [ + ''[a-f0-9]*'' + ''release-5.2'' + ] + [ + ''[a-f0-9]*'' + ''b7a857659f8485ee3c6769c27a3e74b0af910746'' + ] + [ + ''.*pypy.*'' + ''/nix/store/8w718rm43x7z73xhw9d6vh8s4snrq67h-python3-3.12.10/bin/python3.12'' + ] + [ + ''(.*)\.git'' + ''gn'' + ] + [ + ''[a-f0-9]*'' + ''df98b86690c83b81aedc909ded18857296406159'' + ] + [ + ''.*-rc\..*'' + ''22.14.0'' + ] + [ + ''.*/linux-gecko-(.*).tar.bz2'' + ''https://static.replay.io/downloads/linux-gecko-20220722-71c783507536-b7eae18423ef.tar.bz2'' + ] + [ + ''.*/linux-node-(.*)'' + ''https://static.replay.io/downloads/linux-node-20220726-bac6d66b5ca1-5b966f2f136c'' + ] + [ + ''.*-DSQLITE_ENABLE_FTS3.*'' + ''-DSQLITE_ENABLE_COLUMN_METADATA -DSQLITE_ENABLE_DBSTAT_VTAB -DSQLITE_ENABLE_JSON1 -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_FTS3_TOKENIZER -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_PREUPDATE_HOOK -DSQLITE_ENABLE_RTREE -DSQLITE_ENABLE_SESSION -DSQLITE_ENABLE_STMT_SCANSTATUS -DSQLITE_ENABLE_UNLOCK_NOTIFY -DSQLITE_SOUNDEX -DSQLITE_SECURE_DELETE -DSQLITE_MAX_VARIABLE_NUMBER=250000 -DSQLITE_MAX_EXPR_DEPTH=10000'' + ] + [ + '' + [ + ]*(.*[^ + ])[ + ]*'' + '' + 0.3.2308 + '' + ] + [ + ''(.*)\.git'' + ''rtmpdump'' + ] + [ + ''.*;.*'' + ''Game'' + ] + [ + ''.*;.*'' + ''Game'' + ] + [ + ''(.+)+(.+)'' + ''17.0.14+7'' + ] + [ + ''^#(.*)$'' + ''20240715'' + ] + [ + ''[[:alpha:]_][[:alnum:]_]*(\.[[:alpha:]_][[:alnum:]_]*)*'' + ''external_deps_dirs'' + ] + [ + ''[[:alpha:]_][[:alnum:]_]*(\.[[:alpha:]_][[:alnum:]_]*)*'' + ''local_cache'' + ] + [ + ''^#(.*)$'' + ''20240715'' + ] + [ + ''.*-rc\..*'' + ''20.19.2'' + ] + [ + ''(.+)+(.+)'' + ''21.0.7+6'' + ] + [ + ''.*llvm-tblgen.*'' + ''-DLLVM_INSTALL_PACKAGE_DIR:STRING=/02qcpld1y6xhs5gz9bchpxaw0xdhmsp5dv88lh25r2ss44kh8dxz/lib/cmake/llvm'' + ] + [ + ''.*llvm-tblgen.*'' + ''-DLLVM_ENABLE_RTTI:BOOL=TRUE'' + ] + [ + ''.*llvm-tblgen.*'' + ''-DLLVM_TABLEGEN:STRING=/nix/store/xp9hkw8nsw9p81d69yvcg1yr6f7vh71c-llvm-tblgen-18.1.8/bin/llvm-tblgen'' + ] + [ + ''.*llvm-tblgen.*'' + ''-DLLVM_TABLEGEN_EXE:STRING=/nix/store/xp9hkw8nsw9p81d69yvcg1yr6f7vh71c-llvm-tblgen-18.1.8/bin/llvm-tblgen'' + ] + [ + ''(.+)-b(.+)'' + ''8u442-b06'' + ] + [ + ''.*-DSQLITE_ENABLE_FTS3.*'' + ''-DSQLITE_ENABLE_COLUMN_METADATA -DSQLITE_ENABLE_DBSTAT_VTAB -DSQLITE_ENABLE_JSON1 -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_FTS3_TOKENIZER -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_PREUPDATE_HOOK -DSQLITE_ENABLE_RTREE -DSQLITE_ENABLE_SESSION -DSQLITE_ENABLE_STMT_SCANSTATUS -DSQLITE_ENABLE_UNLOCK_NOTIFY -DSQLITE_SOUNDEX -DSQLITE_SECURE_DELETE -DSQLITE_MAX_VARIABLE_NUMBER=250000 -DSQLITE_MAX_EXPR_DEPTH=10000'' + ] + [ + ''([^/]*)/([^/]*)(/SNAPSHOT)?(/.*)?'' + ''jna/5.6.0'' + ] + [ + ''([^/]*)/([^/]*)(/SNAPSHOT)?(/.*)?'' + ''jna/5.6.0'' + ] + [ + ''[0-9]+'' + ''2'' + ] + [ + ''[0-9]+'' + ''3'' + ] + [ + ''[0-9]+'' + ''unstable'' + ] + [ + ''[[:space:]]*0*(-?[[:digit:]]+)[[:space:]]*'' + ''2'' + ] + [ + ''0+'' + ''2'' + ] + [ + ''0+'' + ''0'' + ] + [ + ''.*org/eclipse/jdt/ecj.*'' + ''https://repo.maven.apache.org/maven2/org/eclipse/jdt/ecj/maven-metadata.xml'' + ] + [ + ''.*[<>"'&].*'' + ''org.eclipse.jdt'' + ] + [ + ''.*[<>"'&].*'' + ''20241203050026'' + ] + [ + ''[a-zA-Z_][a-zA-Z0-9_'-]*'' + ''cpu'' + ] + [ + ''[a-zA-Z_][a-zA-Z0-9_'-]*'' + ''bits'' + ] + [ + ''armv[67]l-linux'' + ''x86_64-linux'' + ] + [ + ''armv[67]l-linux'' + ''x86_64-linux'' + ] + [ + ''0+'' + ''0'' + ] + [ + ''[0-9]+'' + ''rc'' + ] + [ + ''.*tensorflow_cpu.*'' + ''https://storage.googleapis.com/tensorflow/versions/2.19.0/tensorflow_cpu-2.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl'' + ] + [ + ''git\+([^?]+)(\?(rev|tag|branch)=(.*))?#(.*)'' + ''git+https://github.com/GRA0007/google-cloud-rs.git#4a2db92efd57a896e14d18877458c6ae43418aec'' + ] + [ + ''git\+([^?]+)(\?(rev|tag|branch)=(.*))?#(.*)'' + ''git+https://github.com/GRA0007/google-cloud-rs.git#4a2db92efd57a896e14d18877458c6ae43418aec'' + ] + [ + ''mpv[-_](.*)'' + ''detect-image'' + ] + [ + ''.*org/bouncycastle/bcutil-lts8on.*'' + ''https://plugins.gradle.org/m2/org/bouncycastle/bcutil-lts8on/maven-metadata.xml'' + ] + [ + ''^.*-unstable-([[:digit:]]{4})-([[:digit:]]{2})-([[:digit:]]{2})$'' + ''0.9.0'' + ] + [ + ''(.+)/(.+)'' + ''rejeep/ansi.el'' + ] + [ + ''^.*-unstable-([[:digit:]]{4})-([[:digit:]]{2})-([[:digit:]]{2})$'' + ''20230306.1823'' + ] + [ + ''(.+)/(.+)'' + ''rejeep/commander.el'' + ] + [ + ''mpv[-_](.*)'' + ''equalizer'' + ] + [ + ''(.*)-([^-]*)-([^-]*)'' + ''2.2.4-20231021.200112-6'' + ] + [ + ''(.*)-([^-]*)-([^-]*)'' + ''2.2.4-20231021.200112-6'' + ] + [ + ''.*com/badlogicgames/gdx-controllers/gdx-controllers-core.*'' + ''https://oss.sonatype.org/content/repositories/snapshots/com/badlogicgames/gdx-controllers/gdx-controllers-core/2.2.4-SNAPSHOT/maven-metadata.xml'' + ] + [ + ''.*com/badlogicgames/gdx-controllers/gdx-controllers-desktop.*'' + ''https://oss.sonatype.org/content/repositories/snapshots/com/badlogicgames/gdx-controllers/gdx-controllers-desktop/2.2.4-SNAPSHOT/maven-metadata.xml'' + ] + [ + ''^(#.*|$)'' + ''.git'' + ] + [ + ''^(#.*|$)'' + ''__pycache__'' + ] + [ + ''^(#.*|$)'' + '''' + ] + [ + ''^(!?)(.*)'' + ''.git'' + ] + [ + ''^(/?)(.*)'' + ''\.git'' + ] + [ + ''.+/.+'' + ''\.git'' + ] + [ + ''^(.*)/$'' + ''(^|.*/)\.git'' + ] + [ + ''(^|.*/)\.git'' + ''.flake8'' + ] + [ + ''^(!?)(.*)'' + ''__pycache__'' + ] + [ + ''^(/?)(.*)'' + ''__pycache__'' + ] + [ + ''.+/.+'' + ''__pycache__'' + ] + [ + ''^(.*)/$'' + ''(^|.*/)__pycache__'' + ] + [ + ''(^|.*/)__pycache__'' + ''.flake8'' + ] + [ + ''^(#.*|$)'' + '''' + ] + [ + ''(^|.*/)\.git'' + ''.gitignore'' + ] + [ + ''(^|.*/)__pycache__'' + ''.gitignore'' + ] + [ + ''^[a-fA-F0-9]{40}$'' + ''3a667bdb3d7f0955a5a51c8468eac83210c1439e'' + ] + [ + ''.*com/android/tools/build/gradle.*'' + ''https://repo.maven.apache.org/maven2/com/android/tools/build/gradle/maven-metadata.xml'' + ] + [ + ''^[a-fA-F0-9]{40}$'' + ''dc0a228a5544988d4a920cfb40be9cd28db41423'' + ] + [ + ''(.+)-b(.+)'' + ''8u442-b06'' + ] + [ + ''.*com/tobiasdiez/easybind.*'' + ''https://oss.sonatype.org/content/groups/public/com/tobiasdiez/easybind/2.2.1-SNAPSHOT/maven-metadata.xml'' + ] + [ + ''.*org/hamcrest/hamcrest.*'' + ''https://repo.maven.apache.org/maven2/org/hamcrest/hamcrest/maven-metadata.xml'' + ] + [ + ''^.*CONFIG_BOARD_DIRECTORY="([a-zA-Z0-9_]+)".*$'' + '' + # CONFIG_LOW_LEVEL_OPTIONS is not set + # CONFIG_MACH_AVR is not set + # CONFIG_MACH_ATSAM is not set + # CONFIG_MACH_ATSAMD is not set + # CONFIG_MACH_LPC176X is not set + # CONFIG_MACH_STM32 is not set + # CONFIG_MACH_HC32F460 is not set + # CONFIG_MACH_RPXXXX is not set + # CONFIG_MACH_PRU is not set + # CONFIG_MACH_AR100 is not set + # CONFIG_MACH_LINUX is not set + CONFIG_MACH_SIMU=y + CONFIG_BOARD_DIRECTORY="simulator" + CONFIG_CLOCK_FREQ=20000000 + CONFIG_SERIAL=y + CONFIG_SIMULATOR_SELECT=y + CONFIG_SERIAL_BAUD=250000 + CONFIG_USB_VENDOR_ID=0x1d50 + CONFIG_USB_DEVICE_ID=0x614e + CONFIG_USB_SERIAL_NUMBER="12345" + CONFIG_WANT_ADC=y + CONFIG_WANT_SPI=y + CONFIG_WANT_SOFTWARE_SPI=y + CONFIG_WANT_HARD_PWM=y + CONFIG_WANT_BUTTONS=y + CONFIG_WANT_TMCUART=y + CONFIG_WANT_NEOPIXEL=y + CONFIG_WANT_PULSE_COUNTER=y + CONFIG_WANT_ST7920=y + CONFIG_WANT_HD44780=y + CONFIG_WANT_ADXL345=y + CONFIG_WANT_LIS2DW=y + CONFIG_WANT_THERMOCOUPLE=y + CONFIG_WANT_HX71X=y + CONFIG_WANT_ADS1220=y + CONFIG_WANT_SENSOR_ANGLE=y + CONFIG_NEED_SENSOR_BULK=y + CONFIG_CANBUS_FREQUENCY=1000000 + CONFIG_INLINE_STEPPER_HACK=y + CONFIG_HAVE_GPIO=y + CONFIG_HAVE_GPIO_ADC=y + CONFIG_HAVE_GPIO_SPI=y + CONFIG_HAVE_GPIO_HARD_PWM=y + '' + ] + [ + ''[^.]*[.][^.]*-.*'' + ''5.15.183-rt85'' + ] + [ + ''[^.]*[.][^.]*-.*'' + ''6.1.134-rt51'' + ] + [ + ''^\.sw[a-z]$'' + ''package.nix'' + ] + [ + ''^\..*\.sw[a-z]$'' + ''package.nix'' + ] + [ + ''^\.sw[a-z]$'' + ''pyproject.toml'' + ] + [ + ''^\..*\.sw[a-z]$'' + ''pyproject.toml'' + ] + [ + ''mpv[-_](.*)'' + ''mpv-notify-send'' + ] + [ + ''mpv[-_](.*)'' + ''mpv-playlistmanager'' + ] + [ + ''.*ch/qos/logback/logback-core.*'' + ''https://repo.maven.apache.org/maven2/ch/qos/logback/logback-core/maven-metadata.xml'' + ] + [ + ''.*commons-codec/commons-codec.*'' + ''https://repo.maven.apache.org/maven2/commons-codec/commons-codec/maven-metadata.xml'' + ] + [ + ''/[0-9a-z]{52}'' + ''/run/opengl-driver'' + ] + [ + ''/[0-9a-z]{52}'' + ''/dev/dri'' + ] + [ + ''<(.*)>'' + ''_module'' + ] + [ + ''<(.*)>'' + ''args'' + ] + [ + ''<(.*)>'' + '''' + ] + [ + ''<(.*)>'' + '''' + ] + [ + ''[a-zA-Z_][a-zA-Z0-9_'-]*'' + ''2bwm'' + ] + [ + ''[a-zA-Z_][a-zA-Z0-9_'-]*'' + ''pm.max_children'' + ] + [ + ''(pypy|python)([[:digit:]]*)'' + ''override'' + ] + [ + ''(pypy|python)([[:digit:]]*)'' + ''overrideDerivation'' + ] + [ + ''(pypy|python)([[:digit:]]*)'' + ''pypy27'' + ] + [ + ''(pypy|python)([[:digit:]]*)'' + ''pypy310'' + ] + [ + ''^ref: (.*)$'' + ''ref: refs/heads/master'' + ] + [ + ''^ref: (.*)$'' + ''f870c6ccc8951fc48aeb293cf3e98ade6ac42668'' + ] + [ + ''^ref: (.*)$'' + ''ref: refs/heads/master'' + ] + [ + ''^ref: (.*)$'' + ''f870c6ccc8951fc48aeb293cf3e98ade6ac42668'' + ] + [ + ''.*\.post[0-9]+'' + ''1.7.2'' + ] + [ + ''.*tensorflow_cpu.*'' + ''https://storage.googleapis.com/tensorflow/versions/2.19.0/tensorflow_cpu-2.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl'' + ] + [ + ''.*tensorflow_cpu.*'' + ''https://storage.googleapis.com/tensorflow/versions/2.19.0/tensorflow-2.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl'' + ] + [ + ''.*\.post[0-9]+'' + ''1.7.2'' + ] + [ + ''.*darwin.*'' + ''i686-cygwin'' + ] + [ + ''.*darwin.*'' + ''x86_64-cygwin'' + ] + [ + ''.*darwin.*'' + ''x86_64-darwin'' + ] + [ + ''.*darwin.*'' + ''aarch64-darwin'' + ] + [ + ''.*com/badlogicgames/gdx-controllers/gdx-controllers-core.*'' + ''https://oss.sonatype.org/content/repositories/snapshots/com/badlogicgames/gdx-controllers/gdx-controllers-core/2.2.4-SNAPSHOT/maven-metadata.xml'' + ] + [ + ''.*com/badlogicgames/gdx-controllers/gdx-controllers-desktop.*'' + ''https://oss.sonatype.org/content/repositories/snapshots/com/badlogicgames/gdx-controllers/gdx-controllers-desktop/2.2.4-SNAPSHOT/maven-metadata.xml'' + ] + [ + ''.*/linux-recordreplay-(.*).tgz'' + ''https://static.replay.io/downloads/linux-recordreplay-b7eae18423ef.tgz'' + ] + [ + ''.*/linux-node-(.*)'' + ''https://static.replay.io/downloads/linux-node-20220726-bac6d66b5ca1-5b966f2f136c'' + ] + [ + ''.*-large-wordlist.*'' + ''hunspell-dict-cs-cz-libreoffice-6.3.0.4'' + ] + [ + ''.*-large-wordlist.*'' + ''hunspell-dict-da-dk-2.5.189'' + ] + [ + ''.*-large-wordlist.*'' + ''hunspell-dict-en-au-large-wordlist-2018.04.16'' + ] + [ + ''.*-large-wordlist.*'' + ''hunspell-dict-en-ca-large-wordlist-2018.04.16'' + ] + [ + ''.*com/fazecast/jSerialComm.*'' + ''https://oss.sonatype.org/content/repositories/snapshots/com/fazecast/jSerialComm/2.11.1-SNAPSHOT/maven-metadata.xml'' + ] + [ + ''.*net/java/dev/jna/jna-platform.*'' + ''https://oss.sonatype.org/content/repositories/snapshots/net/java/dev/jna/jna-platform/5.1.1-SNAPSHOT/maven-metadata.xml'' + ] + [ + ''.*net/java/dev/jna/jna-platform.*'' + ''https://oss.sonatype.org/content/repositories/snapshots/net/java/dev/jna/jna-platform/maven-metadata.xml'' + ] + [ + ''.*net/java/dev/jna/jna.*'' + ''https://oss.sonatype.org/content/repositories/snapshots/net/java/dev/jna/jna/5.1.1-SNAPSHOT/maven-metadata.xml'' + ] + [ + ''.*net/java/dev/jna/jna.*'' + ''https://oss.sonatype.org/content/repositories/snapshots/net/java/dev/jna/jna/maven-metadata.xml'' + ] + [ + ''.*org/java-websocket/Java-WebSocket.*'' + ''https://oss.sonatype.org/content/repositories/snapshots/org/java-websocket/Java-WebSocket/1.3.10-SNAPSHOT/maven-metadata.xml'' + ] + [ + ''.*org/java-websocket/Java-WebSocket.*'' + ''https://oss.sonatype.org/content/repositories/snapshots/org/java-websocket/Java-WebSocket/maven-metadata.xml'' + ] + [ + ''.*com/melloware/jintellitype.*'' + ''https://repo.maven.apache.org/maven2/com/melloware/jintellitype/maven-metadata.xml'' + ] + [ + ''[0-9.]*([a-z]*)'' + ''2025.1.1'' + ] + [ + ''.*com/velocitypowered/velocity-brigadier.*'' + ''https://repo.papermc.io/repository/maven-public/com/velocitypowered/velocity-brigadier/1.0.0-SNAPSHOT/maven-metadata.xml'' + ] + ] diff --git a/tests/functional/lang/eval-okay-regex-split.nix b/tests/functional/lang/eval-okay-regex-split.nix index 0073e057787..8ab3e60cbb2 100644 --- a/tests/functional/lang/eval-okay-regex-split.nix +++ b/tests/functional/lang/eval-okay-regex-split.nix @@ -1,48 +1,197 @@ with builtins; # Non capturing regex returns empty lists -assert split "foobar" "foobar" == ["" [] ""]; -assert split "fo*" "f" == ["" [] ""]; -assert split "fo+" "f" == ["f"]; -assert split "fo*" "fo" == ["" [] ""]; -assert split "fo*" "foo" == ["" [] ""]; -assert split "fo+" "foo" == ["" [] ""]; -assert split "fo{1,2}" "foo" == ["" [] ""]; -assert split "fo{1,2}" "fooo" == ["" [] "o"]; -assert split "fo*" "foobar" == ["" [] "bar"]; +assert + split "foobar" "foobar" == [ + "" + [ ] + "" + ]; +assert + split "fo*" "f" == [ + "" + [ ] + "" + ]; +assert split "fo+" "f" == [ "f" ]; +assert + split "fo*" "fo" == [ + "" + [ ] + "" + ]; +assert + split "fo*" "foo" == [ + "" + [ ] + "" + ]; +assert + split "fo+" "foo" == [ + "" + [ ] + "" + ]; +assert + split "fo{1,2}" "foo" == [ + "" + [ ] + "" + ]; +assert + split "fo{1,2}" "fooo" == [ + "" + [ ] + "o" + ]; +assert + split "fo*" "foobar" == [ + "" + [ ] + "bar" + ]; # Capturing regex returns a list of sub-matches -assert split "(fo*)" "f" == ["" ["f"] ""]; -assert split "(fo+)" "f" == ["f"]; -assert split "(fo*)" "fo" == ["" ["fo"] ""]; -assert split "(f)(o*)" "f" == ["" ["f" ""] ""]; -assert split "(f)(o*)" "foo" == ["" ["f" "oo"] ""]; -assert split "(fo+)" "foo" == ["" ["foo"] ""]; -assert split "(fo{1,2})" "foo" == ["" ["foo"] ""]; -assert split "(fo{1,2})" "fooo" == ["" ["foo"] "o"]; -assert split "(fo*)" "foobar" == ["" ["foo"] "bar"]; +assert + split "(fo*)" "f" == [ + "" + [ "f" ] + "" + ]; +assert split "(fo+)" "f" == [ "f" ]; +assert + split "(fo*)" "fo" == [ + "" + [ "fo" ] + "" + ]; +assert + split "(f)(o*)" "f" == [ + "" + [ + "f" + "" + ] + "" + ]; +assert + split "(f)(o*)" "foo" == [ + "" + [ + "f" + "oo" + ] + "" + ]; +assert + split "(fo+)" "foo" == [ + "" + [ "foo" ] + "" + ]; +assert + split "(fo{1,2})" "foo" == [ + "" + [ "foo" ] + "" + ]; +assert + split "(fo{1,2})" "fooo" == [ + "" + [ "foo" ] + "o" + ]; +assert + split "(fo*)" "foobar" == [ + "" + [ "foo" ] + "bar" + ]; # Matches are greedy. -assert split "(o+)" "oooofoooo" == ["" ["oooo"] "f" ["oooo"] ""]; +assert + split "(o+)" "oooofoooo" == [ + "" + [ "oooo" ] + "f" + [ "oooo" ] + "" + ]; # Matches multiple times. -assert split "(b)" "foobarbaz" == ["foo" ["b"] "ar" ["b"] "az"]; +assert + split "(b)" "foobarbaz" == [ + "foo" + [ "b" ] + "ar" + [ "b" ] + "az" + ]; # Split large strings containing newlines. null are inserted when a # pattern within the current did not match anything. -assert split "[[:space:]]+|([',.!?])" '' - Nix Rocks! - That's why I use it. -'' == [ - "Nix" [ null ] "Rocks" ["!"] "" [ null ] - "That" ["'"] "s" [ null ] "why" [ null ] "I" [ null ] "use" [ null ] "it" ["."] "" [ null ] - "" -]; +assert + split "[[:space:]]+|([',.!?])" '' + Nix Rocks! + That's why I use it. + '' == [ + "Nix" + [ null ] + "Rocks" + [ "!" ] + "" + [ null ] + "That" + [ "'" ] + "s" + [ null ] + "why" + [ null ] + "I" + [ null ] + "use" + [ null ] + "it" + [ "." ] + "" + [ null ] + "" + ]; # Documentation examples -assert split "(a)b" "abc" == [ "" [ "a" ] "c" ]; -assert split "([ac])" "abc" == [ "" [ "a" ] "b" [ "c" ] "" ]; -assert split "(a)|(c)" "abc" == [ "" [ "a" null ] "b" [ null "c" ] "" ]; -assert split "([[:upper:]]+)" " FOO " == [ " " [ "FOO" ] " " ]; +assert + split "(a)b" "abc" == [ + "" + [ "a" ] + "c" + ]; +assert + split "([ac])" "abc" == [ + "" + [ "a" ] + "b" + [ "c" ] + "" + ]; +assert + split "(a)|(c)" "abc" == [ + "" + [ + "a" + null + ] + "b" + [ + null + "c" + ] + "" + ]; +assert + split "([[:upper:]]+)" " FOO " == [ + " " + [ "FOO" ] + " " + ]; true diff --git a/tests/functional/lang/eval-okay-regression-20220125.nix b/tests/functional/lang/eval-okay-regression-20220125.nix index 48550237394..1c4b8e09f39 100644 --- a/tests/functional/lang/eval-okay-regression-20220125.nix +++ b/tests/functional/lang/eval-okay-regression-20220125.nix @@ -1,2 +1 @@ ((__curPosFoo: __curPosFoo) 1) + ((__curPosBar: __curPosBar) 2) - diff --git a/tests/functional/lang/eval-okay-regrettable-rec-attrset-merge.nix b/tests/functional/lang/eval-okay-regrettable-rec-attrset-merge.nix index 8df6a2ad81d..e92ae8125a6 100644 --- a/tests/functional/lang/eval-okay-regrettable-rec-attrset-merge.nix +++ b/tests/functional/lang/eval-okay-regrettable-rec-attrset-merge.nix @@ -1,3 +1,10 @@ # This is for backwards compatibility, not because we like it. # See https://github.com/NixOS/nix/issues/9020. -{ a = rec { b = c + 1; d = 2; }; a.c = d + 3; }.a.b +{ + a = rec { + b = c + 1; + d = 2; + }; + a.c = d + 3; +} +.a.b diff --git a/tests/functional/lang/eval-okay-remove.nix b/tests/functional/lang/eval-okay-remove.nix index 4ad5ba897fa..a7ee3a07148 100644 --- a/tests/functional/lang/eval-okay-remove.nix +++ b/tests/functional/lang/eval-okay-remove.nix @@ -1,5 +1,8 @@ let { - attrs = {x = 123; y = 456;}; + attrs = { + x = 123; + y = 456; + }; - body = (removeAttrs attrs ["x"]).y; -} \ No newline at end of file + body = (removeAttrs attrs [ "x" ]).y; +} diff --git a/tests/functional/lang/eval-okay-repeated-empty-attrs.nix b/tests/functional/lang/eval-okay-repeated-empty-attrs.nix index 030a3b85c76..0749e21a57c 100644 --- a/tests/functional/lang/eval-okay-repeated-empty-attrs.nix +++ b/tests/functional/lang/eval-okay-repeated-empty-attrs.nix @@ -1,2 +1,5 @@ # Tests that empty attribute sets are not printed as `«repeated»`. -[ {} {} ] +[ + { } + { } +] diff --git a/tests/functional/lang/eval-okay-repeated-empty-list.nix b/tests/functional/lang/eval-okay-repeated-empty-list.nix index 376c51be886..7e24fe81b27 100644 --- a/tests/functional/lang/eval-okay-repeated-empty-list.nix +++ b/tests/functional/lang/eval-okay-repeated-empty-list.nix @@ -1 +1,4 @@ -[ [] [] ] +[ + [ ] + [ ] +] diff --git a/tests/functional/lang/eval-okay-replacestrings.nix b/tests/functional/lang/eval-okay-replacestrings.nix index a803e65199a..81a932a1daa 100644 --- a/tests/functional/lang/eval-okay-replacestrings.nix +++ b/tests/functional/lang/eval-okay-replacestrings.nix @@ -1,12 +1,13 @@ with builtins; -[ (replaceStrings ["o"] ["a"] "foobar") - (replaceStrings ["o"] [""] "foobar") - (replaceStrings ["oo"] ["u"] "foobar") - (replaceStrings ["oo" "a"] ["a" "oo"] "foobar") - (replaceStrings ["oo" "oo"] ["u" "i"] "foobar") - (replaceStrings [""] ["X"] "abc") - (replaceStrings [""] ["X"] "") - (replaceStrings ["-"] ["_"] "a-b") - (replaceStrings ["oo" "XX"] ["u" (throw "unreachable")] "foobar") +[ + (replaceStrings [ "o" ] [ "a" ] "foobar") + (replaceStrings [ "o" ] [ "" ] "foobar") + (replaceStrings [ "oo" ] [ "u" ] "foobar") + (replaceStrings [ "oo" "a" ] [ "a" "oo" ] "foobar") + (replaceStrings [ "oo" "oo" ] [ "u" "i" ] "foobar") + (replaceStrings [ "" ] [ "X" ] "abc") + (replaceStrings [ "" ] [ "X" ] "") + (replaceStrings [ "-" ] [ "_" ] "a-b") + (replaceStrings [ "oo" "XX" ] [ "u" (throw "unreachable") ] "foobar") ] diff --git a/tests/functional/lang/eval-okay-scope-1.nix b/tests/functional/lang/eval-okay-scope-1.nix index fa38a7174e0..b7bbcc432d5 100644 --- a/tests/functional/lang/eval-okay-scope-1.nix +++ b/tests/functional/lang/eval-okay-scope-1.nix @@ -1,6 +1,13 @@ -(({x}: x: +( + ( + { x }: + x: - { x = 1; - y = x; - } -) {x = 2;} 3).y + { + x = 1; + y = x; + } + ) + { x = 2; } + 3 +).y diff --git a/tests/functional/lang/eval-okay-scope-2.nix b/tests/functional/lang/eval-okay-scope-2.nix index eb8b02bc499..54f7ec3b230 100644 --- a/tests/functional/lang/eval-okay-scope-2.nix +++ b/tests/functional/lang/eval-okay-scope-2.nix @@ -1,6 +1,12 @@ -((x: {x}: - rec { - x = 1; - y = x; - } -) 2 {x = 3;}).y +( + ( + x: + { x }: + rec { + x = 1; + y = x; + } + ) + 2 + { x = 3; } +).y diff --git a/tests/functional/lang/eval-okay-scope-3.nix b/tests/functional/lang/eval-okay-scope-3.nix index 10d6bc04d83..6a77583b7da 100644 --- a/tests/functional/lang/eval-okay-scope-3.nix +++ b/tests/functional/lang/eval-okay-scope-3.nix @@ -1,6 +1,13 @@ -((x: as: {x}: - rec { - inherit (as) x; - y = x; - } -) 2 {x = 4;} {x = 3;}).y +( + ( + x: as: + { x }: + rec { + inherit (as) x; + y = x; + } + ) + 2 + { x = 4; } + { x = 3; } +).y diff --git a/tests/functional/lang/eval-okay-scope-4.nix b/tests/functional/lang/eval-okay-scope-4.nix index dc8243bc854..ccae8564cda 100644 --- a/tests/functional/lang/eval-okay-scope-4.nix +++ b/tests/functional/lang/eval-okay-scope-4.nix @@ -3,8 +3,13 @@ let { x = "a"; y = "b"; - f = {x ? y, y ? x}: x + y; - - body = f {x = "c";} + f {y = "d";}; + f = + { + x ? y, + y ? x, + }: + x + y; + + body = f { x = "c"; } + f { y = "d"; }; } diff --git a/tests/functional/lang/eval-okay-scope-6.nix b/tests/functional/lang/eval-okay-scope-6.nix index 0995d4e7e7e..be2cc31a1f2 100644 --- a/tests/functional/lang/eval-okay-scope-6.nix +++ b/tests/functional/lang/eval-okay-scope-6.nix @@ -1,7 +1,12 @@ let { - f = {x ? y, y ? x}: x + y; + f = + { + x ? y, + y ? x, + }: + x + y; - body = f {x = "c";} + f {y = "d";}; + body = f { x = "c"; } + f { y = "d"; }; } diff --git a/tests/functional/lang/eval-okay-scope-7.nix b/tests/functional/lang/eval-okay-scope-7.nix index 4da02968f6b..91f22f55388 100644 --- a/tests/functional/lang/eval-okay-scope-7.nix +++ b/tests/functional/lang/eval-okay-scope-7.nix @@ -3,4 +3,5 @@ rec { x = { y = 1; }; -}.y +} +.y diff --git a/tests/functional/lang/eval-okay-search-path.nix b/tests/functional/lang/eval-okay-search-path.nix index 6fe33decc01..702e1b64c15 100644 --- a/tests/functional/lang/eval-okay-search-path.nix +++ b/tests/functional/lang/eval-okay-search-path.nix @@ -6,5 +6,16 @@ assert isFunction (import ); assert length __nixPath == 5; assert length (filter (x: baseNameOf x.path == "dir4") __nixPath) == 1; -import + import + import + import - + (let __nixPath = [ { path = ./dir2; } { path = ./dir1; } ]; in import ) +import ++ import ++ import ++ import ++ ( + let + __nixPath = [ + { path = ./dir2; } + { path = ./dir1; } + ]; + in + import +) diff --git a/tests/functional/lang/eval-okay-sort.exp b/tests/functional/lang/eval-okay-sort.exp index 899119e20e3..fcb3b2224f6 100644 --- a/tests/functional/lang/eval-okay-sort.exp +++ b/tests/functional/lang/eval-okay-sort.exp @@ -1 +1 @@ -[ [ 42 77 147 249 483 526 ] [ 526 483 249 147 77 42 ] [ "bar" "fnord" "foo" "xyzzy" ] [ { key = 1; value = "foo"; } { key = 1; value = "fnord"; } { key = 2; value = "bar"; } ] [ [ ] [ ] [ 1 ] [ 1 4 ] [ 1 5 ] [ 1 6 ] [ 2 ] [ 2 3 ] [ 3 ] [ 3 ] ] ] +[ [ 42 77 147 249 483 526 ] [ 526 483 249 147 77 42 ] [ "bar" "fnord" "foo" "xyzzy" ] [ { key = 1; value = "foo"; } { key = 1; value = "fnord"; } { key = 2; value = "bar"; } ] [ { key = 1; value = "foo"; } { key = 1; value = "foo2"; } { key = 1; value = "foo3"; } { key = 1; value = "foo4"; } { key = 1; value = "foo5"; } { key = 1; value = "foo6"; } { key = 1; value = "foo7"; } { key = 1; value = "foo8"; } { key = 2; value = "bar"; } { key = 2; value = "bar2"; } { key = 2; value = "bar3"; } { key = 2; value = "bar4"; } { key = 2; value = "bar5"; } { key = 3; value = "baz"; } { key = 3; value = "baz2"; } { key = 3; value = "baz3"; } { key = 3; value = "baz4"; } { key = 4; value = "biz1"; } ] [ [ ] [ ] [ 1 ] [ 1 4 ] [ 1 5 ] [ 1 6 ] [ 2 ] [ 2 3 ] [ 3 ] [ 3 ] ] ] diff --git a/tests/functional/lang/eval-okay-sort.nix b/tests/functional/lang/eval-okay-sort.nix index 50aa78e4032..7a3b7f71b19 100644 --- a/tests/functional/lang/eval-okay-sort.nix +++ b/tests/functional/lang/eval-okay-sort.nix @@ -1,20 +1,138 @@ with builtins; -[ (sort lessThan [ 483 249 526 147 42 77 ]) - (sort (x: y: y < x) [ 483 249 526 147 42 77 ]) - (sort lessThan [ "foo" "bar" "xyzzy" "fnord" ]) - (sort (x: y: x.key < y.key) - [ { key = 1; value = "foo"; } { key = 2; value = "bar"; } { key = 1; value = "fnord"; } ]) +[ (sort lessThan [ - [ 1 6 ] + 483 + 249 + 526 + 147 + 42 + 77 + ]) + (sort (x: y: y < x) [ + 483 + 249 + 526 + 147 + 42 + 77 + ]) + (sort lessThan [ + "foo" + "bar" + "xyzzy" + "fnord" + ]) + (sort (x: y: x.key < y.key) [ + { + key = 1; + value = "foo"; + } + { + key = 2; + value = "bar"; + } + { + key = 1; + value = "fnord"; + } + ]) + (sort (x: y: x.key < y.key) [ + { + key = 1; + value = "foo"; + } + { + key = 2; + value = "bar"; + } + { + key = 1; + value = "foo2"; + } + { + key = 2; + value = "bar2"; + } + { + key = 2; + value = "bar3"; + } + { + key = 2; + value = "bar4"; + } + { + key = 1; + value = "foo3"; + } + { + key = 3; + value = "baz"; + } + { + key = 3; + value = "baz2"; + } + { + key = 1; + value = "foo4"; + } + { + key = 3; + value = "baz3"; + } + { + key = 1; + value = "foo5"; + } + { + key = 1; + value = "foo6"; + } + { + key = 2; + value = "bar5"; + } + { + key = 3; + value = "baz4"; + } + { + key = 1; + value = "foo7"; + } + { + key = 4; + value = "biz1"; + } + { + key = 1; + value = "foo8"; + } + ]) + (sort lessThan [ + [ + 1 + 6 + ] [ ] - [ 2 3 ] + [ + 2 + 3 + ] [ 3 ] - [ 1 5 ] + [ + 1 + 5 + ] [ 2 ] [ 1 ] [ ] - [ 1 4 ] + [ + 1 + 4 + ] [ 3 ] ]) ] diff --git a/tests/functional/lang/eval-okay-string.nix b/tests/functional/lang/eval-okay-string.nix index 47cc989ad46..d3b743fdbed 100644 --- a/tests/functional/lang/eval-okay-string.nix +++ b/tests/functional/lang/eval-okay-string.nix @@ -1,12 +1,13 @@ -"foo" + "bar" - + toString (/a/b + /c/d) - + toString (/foo/bar + "/../xyzzy/." + "/foo.txt") - + ("/../foo" + toString /x/y) - + "escape: \"quote\" \n \\" - + "end +"foo" ++ "bar" ++ toString (/a/b + /c/d) ++ toString (/foo/bar + "/../xyzzy/." + "/foo.txt") ++ ("/../foo" + toString /x/y) ++ "escape: \"quote\" \n \\" ++ "end of line" - + "foo${if true then "b${"a" + "r"}" else "xyzzy"}blaat" - + "foo$bar" - + "$\"$\"" - + "$" ++ "foo${if true then "b${"a" + "r"}" else "xyzzy"}blaat" ++ "foo$bar" ++ "$\"$\"" ++ "$" diff --git a/tests/functional/lang/eval-okay-strings-as-attrs-names.nix b/tests/functional/lang/eval-okay-strings-as-attrs-names.nix index 5e40928dbe3..158dc8e754e 100644 --- a/tests/functional/lang/eval-okay-strings-as-attrs-names.nix +++ b/tests/functional/lang/eval-okay-strings-as-attrs-names.nix @@ -14,7 +14,5 @@ let # variable. "foo bar" = 1; -in t1 == "test" - && t2 == "caseok" - && t3 == true - && t4 == ["key 1"] +in +t1 == "test" && t2 == "caseok" && t3 == true && t4 == [ "key 1" ] diff --git a/tests/functional/lang/eval-okay-substring-context.nix b/tests/functional/lang/eval-okay-substring-context.nix index d0ef70d4e67..9e9d3a1aa95 100644 --- a/tests/functional/lang/eval-okay-substring-context.nix +++ b/tests/functional/lang/eval-okay-substring-context.nix @@ -2,10 +2,15 @@ with builtins; let - s = "${builtins.derivation { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; }}"; + s = "${builtins.derivation { + name = "test"; + builder = "/bin/sh"; + system = "x86_64-linux"; + }}"; in -if getContext s == getContext "${substring 0 0 s + unsafeDiscardStringContext s}" -then "okay" -else throw "empty substring should preserve context" +if getContext s == getContext "${substring 0 0 s + unsafeDiscardStringContext s}" then + "okay" +else + throw "empty substring should preserve context" diff --git a/tests/functional/lang/eval-okay-tail-call-1.nix b/tests/functional/lang/eval-okay-tail-call-1.nix index a3962ce3fdb..d3ec0c9adfd 100644 --- a/tests/functional/lang/eval-okay-tail-call-1.nix +++ b/tests/functional/lang/eval-okay-tail-call-1.nix @@ -1,3 +1,4 @@ let f = n: if n == 100000 then n else f (n + 1); -in f 0 +in +f 0 diff --git a/tests/functional/lang/eval-okay-tojson.nix b/tests/functional/lang/eval-okay-tojson.nix index ce67943bead..863c0766392 100644 --- a/tests/functional/lang/eval-okay-tojson.nix +++ b/tests/functional/lang/eval-okay-tojson.nix @@ -1,13 +1,26 @@ -builtins.toJSON - { a = 123; - b = -456; - c = "foo"; - d = "foo\n\"bar\""; - e = true; - f = false; - g = [ 1 2 3 ]; - h = [ "a" [ "b" { "foo\nbar" = {}; } ] ]; - i = 1 + 2; - j = 1.44; - k = { __toString = self: self.a; a = "foo"; }; - } +builtins.toJSON { + a = 123; + b = -456; + c = "foo"; + d = "foo\n\"bar\""; + e = true; + f = false; + g = [ + 1 + 2 + 3 + ]; + h = [ + "a" + [ + "b" + { "foo\nbar" = { }; } + ] + ]; + i = 1 + 2; + j = 1.44; + k = { + __toString = self: self.a; + a = "foo"; + }; +} diff --git a/tests/functional/lang/eval-okay-toxml2.nix b/tests/functional/lang/eval-okay-toxml2.nix index ff1791b30eb..0d5989a50e7 100644 --- a/tests/functional/lang/eval-okay-toxml2.nix +++ b/tests/functional/lang/eval-okay-toxml2.nix @@ -1 +1,8 @@ -builtins.toXML [("a" + "b") 10 (rec {x = "x"; y = x;})] +builtins.toXML [ + ("a" + "b") + 10 + (rec { + x = "x"; + y = x; + }) +] diff --git a/tests/functional/lang/eval-okay-tryeval.nix b/tests/functional/lang/eval-okay-tryeval.nix index 629bc440a85..22b23d88342 100644 --- a/tests/functional/lang/eval-okay-tryeval.nix +++ b/tests/functional/lang/eval-okay-tryeval.nix @@ -1,5 +1,8 @@ { x = builtins.tryEval "x"; - y = builtins.tryEval (assert false; "y"); + y = builtins.tryEval ( + assert false; + "y" + ); z = builtins.tryEval (throw "bla"); } diff --git a/tests/functional/lang/eval-okay-types.nix b/tests/functional/lang/eval-okay-types.nix index 9b58be5d1dd..0814489edd3 100644 --- a/tests/functional/lang/eval-okay-types.nix +++ b/tests/functional/lang/eval-okay-types.nix @@ -1,6 +1,7 @@ with builtins; -[ (isNull null) +[ + (isNull null) (isNull (x: x)) (isFunction (x: x)) (isFunction "fnord") @@ -29,7 +30,11 @@ with builtins; (typeOf "xyzzy") (typeOf null) (typeOf { x = 456; }) - (typeOf [ 1 2 3 ]) + (typeOf [ + 1 + 2 + 3 + ]) (typeOf (x: x)) (typeOf ((x: y: x) 1)) (typeOf map) diff --git a/tests/functional/lang/eval-okay-versions.nix b/tests/functional/lang/eval-okay-versions.nix index e9111f5f433..3456015e538 100644 --- a/tests/functional/lang/eval-okay-versions.nix +++ b/tests/functional/lang/eval-okay-versions.nix @@ -10,10 +10,13 @@ let lt = builtins.sub 0 1; gt = 1; - versionTest = v1: v2: expected: - let d1 = builtins.compareVersions v1 v2; - d2 = builtins.compareVersions v2 v1; - in d1 == builtins.sub 0 d2 && d1 == expected; + versionTest = + v1: v2: expected: + let + d1 = builtins.compareVersions v1 v2; + d2 = builtins.compareVersions v2 v1; + in + d1 == builtins.sub 0 d2 && d1 == expected; tests = [ ((builtins.parseDrvName name1).name == "hello") @@ -40,4 +43,5 @@ let (versionTest "2.3pre1" "2.3q" lt) ]; -in (import ./lib.nix).and tests +in +(import ./lib.nix).and tests diff --git a/tests/functional/lang/eval-okay-xml.nix b/tests/functional/lang/eval-okay-xml.nix index 9ee9f8a0b4f..9785c66ef42 100644 --- a/tests/functional/lang/eval-okay-xml.nix +++ b/tests/functional/lang/eval-okay-xml.nix @@ -10,12 +10,31 @@ rec { c = "foo" + "bar"; - f = {z, x, y}: if y then x else z; + f = + { + z, + x, + y, + }: + if y then x else z; id = x: x; - at = args@{x, y, z}: x; - - ellipsis = {x, y, z, ...}: x; + at = + args@{ + x, + y, + z, + }: + x; + + ellipsis = + { + x, + y, + z, + ... + }: + x; } diff --git a/tests/functional/lang/eval-okay-zipAttrsWith.nix b/tests/functional/lang/eval-okay-zipAttrsWith.nix index 877d4e5fa31..20f6891115e 100644 --- a/tests/functional/lang/eval-okay-zipAttrsWith.nix +++ b/tests/functional/lang/eval-okay-zipAttrsWith.nix @@ -3,7 +3,6 @@ with import ./lib.nix; let str = builtins.hashString "sha256" "test"; in -builtins.zipAttrsWith - (n: v: { inherit n v; }) - (map (n: { ${builtins.substring n 1 str} = n; }) - (range 0 31)) +builtins.zipAttrsWith (n: v: { inherit n v; }) ( + map (n: { ${builtins.substring n 1 str} = n; }) (range 0 31) +) diff --git a/tests/functional/lang/lib.nix b/tests/functional/lang/lib.nix index 028a538314b..126128abe7a 100644 --- a/tests/functional/lang/lib.nix +++ b/tests/functional/lang/lib.nix @@ -2,60 +2,76 @@ with builtins; rec { - fold = op: nul: list: - if list == [] - then nul - else op (head list) (fold op nul (tail list)); + fold = + op: nul: list: + if list == [ ] then nul else op (head list) (fold op nul (tail list)); - concat = - fold (x: y: x + y) ""; + concat = fold (x: y: x + y) ""; and = fold (x: y: x && y) true; - flatten = x: - if isList x - then fold (x: y: (flatten x) ++ y) [] x - else [x]; + flatten = x: if isList x then fold (x: y: (flatten x) ++ y) [ ] x else [ x ]; sum = foldl' (x: y: add x y) 0; - hasSuffix = ext: fileName: - let lenFileName = stringLength fileName; - lenExt = stringLength ext; - in !(lessThan lenFileName lenExt) && - substring (sub lenFileName lenExt) lenFileName fileName == ext; + hasSuffix = + ext: fileName: + let + lenFileName = stringLength fileName; + lenExt = stringLength ext; + in + !(lessThan lenFileName lenExt) && substring (sub lenFileName lenExt) lenFileName fileName == ext; # Split a list at the given position. - splitAt = pos: list: - if pos == 0 then {first = []; second = list;} else - if list == [] then {first = []; second = [];} else - let res = splitAt (sub pos 1) (tail list); - in {first = [(head list)] ++ res.first; second = res.second;}; + splitAt = + pos: list: + if pos == 0 then + { + first = [ ]; + second = list; + } + else if list == [ ] then + { + first = [ ]; + second = [ ]; + } + else + let + res = splitAt (sub pos 1) (tail list); + in + { + first = [ (head list) ] ++ res.first; + second = res.second; + }; # Stable merge sort. - sortBy = comp: list: - if lessThan 1 (length list) - then + sortBy = + comp: list: + if lessThan 1 (length list) then let split = splitAt (div (length list) 2) list; first = sortBy comp split.first; second = sortBy comp split.second; - in mergeLists comp first second - else list; + in + mergeLists comp first second + else + list; - mergeLists = comp: list1: list2: - if list1 == [] then list2 else - if list2 == [] then list1 else - if comp (head list2) (head list1) then [(head list2)] ++ mergeLists comp list1 (tail list2) else - [(head list1)] ++ mergeLists comp (tail list1) list2; + mergeLists = + comp: list1: list2: + if list1 == [ ] then + list2 + else if list2 == [ ] then + list1 + else if comp (head list2) (head list1) then + [ (head list2) ] ++ mergeLists comp list1 (tail list2) + else + [ (head list1) ] ++ mergeLists comp (tail list1) list2; id = x: x; const = x: y: x; - range = first: last: - if first > last - then [] - else genList (n: first + n) (last - first + 1); + range = first: last: if first > last then [ ] else genList (n: first + n) (last - first + 1); } diff --git a/tests/functional/lang/non-eval-trivial-lambda-formals.nix b/tests/functional/lang/non-eval-trivial-lambda-formals.nix new file mode 100644 index 00000000000..46a7ea4f494 --- /dev/null +++ b/tests/functional/lang/non-eval-trivial-lambda-formals.nix @@ -0,0 +1 @@ +{ a }: a diff --git a/tests/functional/linux-sandbox-cert-test.nix b/tests/functional/linux-sandbox-cert-test.nix index 2fc083ea932..82989c64f88 100644 --- a/tests/functional/linux-sandbox-cert-test.nix +++ b/tests/functional/linux-sandbox-cert-test.nix @@ -22,9 +22,12 @@ mkDerivation ( # derivations being cached, and do not want to compute the right hash. false; ''; - } // { - fixed-output = { outputHash = "sha256:0000000000000000000000000000000000000000000000000000000000000000"; }; + } + // { + fixed-output = { + outputHash = "sha256:0000000000000000000000000000000000000000000000000000000000000000"; + }; normal = { }; - }.${mode} + } + .${mode} ) - diff --git a/tests/functional/linux-sandbox.sh b/tests/functional/linux-sandbox.sh index 81ef3623796..abb635f1195 100755 --- a/tests/functional/linux-sandbox.sh +++ b/tests/functional/linux-sandbox.sh @@ -9,6 +9,7 @@ TODO_NixOS clearStore requireSandboxSupport +requiresUnprivilegedUserNamespaces # Note: we need to bind-mount $SHELL into the chroot. Currently we # only support the case where $SHELL is in the Nix store, because diff --git a/tests/functional/local-overlay-store/bad-uris.sh b/tests/functional/local-overlay-store/bad-uris.sh index b7930e32e24..f0c6a151c35 100644 --- a/tests/functional/local-overlay-store/bad-uris.sh +++ b/tests/functional/local-overlay-store/bad-uris.sh @@ -19,7 +19,7 @@ TODO_NixOS for i in "${storesBad[@]}"; do echo $i - unshare --mount --map-root-user bash <&1 | grepQuiet 'building.*dependencies-top.drv' + jq < "$TEST_ROOT/log.json" + grep '{"action":"start","fields":\[".*-dependencies-top.drv","",1,1\],"id":.*,"level":3,"parent":0' "$TEST_ROOT/log.json" >&2 + (( $(grep '{"action":"msg","level":5,"msg":"executing builder .*"}' "$TEST_ROOT/log.json" | wc -l) == 5 )) +fi diff --git a/tests/functional/logging/unusual-logging.nix b/tests/functional/logging/unusual-logging.nix new file mode 100644 index 00000000000..ddb8aa53010 --- /dev/null +++ b/tests/functional/logging/unusual-logging.nix @@ -0,0 +1,16 @@ +let + inherit (import ../config.nix) mkDerivation; +in +mkDerivation { + name = "unusual-logging"; + buildCommand = '' + { + echo "@nix 1" + echo "@nix {}" + echo '@nix {"action": null}' + echo '@nix {"action": 123}' + echo '@nix ][' + } >&$NIX_LOG_FD + touch $out + ''; +} diff --git a/tests/functional/meson.build b/tests/functional/meson.build index 933595cd5ef..501ed45c79f 100644 --- a/tests/functional/meson.build +++ b/tests/functional/meson.build @@ -4,8 +4,6 @@ project('nix-functional-tests', 'cpp_std=c++2a', # TODO(Qyriad): increase the warning level 'warning_level=1', - 'debug=true', - 'optimization=2', 'errorlogs=true', # Please print logs for tests that fail ], meson_version : '>= 1.3', @@ -39,7 +37,7 @@ test_confdata = { # Done as a subdir() so Meson places it under `common` in the build directory as well. subdir('common') -config_nix_in = configure_file( +configure_file( input : 'config.nix.in', output : 'config.nix', configuration : test_confdata, @@ -75,6 +73,7 @@ suites = [ 'gc-runtime.sh', 'tarball.sh', 'fetchGit.sh', + 'fetchGitShallow.sh', 'fetchurl.sh', 'fetchPath.sh', 'fetchTree-file.sh', @@ -134,7 +133,8 @@ suites = [ 'nix-copy-ssh-ng.sh', 'post-hook.sh', 'function-trace.sh', - 'fmt.sh', + 'formatter.sh', + 'flamegraph-profiler.sh', 'eval-store.sh', 'why-depends.sh', 'derivation-json.sh', @@ -145,6 +145,7 @@ suites = [ 'placeholders.sh', 'ssh-relay.sh', 'build.sh', + 'build-cores.sh', 'build-delete.sh', 'output-normalization.sh', 'selfref-gc.sh', @@ -159,6 +160,7 @@ suites = [ 'impure-derivations.sh', 'path-from-hash-part.sh', 'path-info.sh', + 'json.sh', 'toString-path.sh', 'read-only-store.sh', 'nested-sandboxing.sh', @@ -166,6 +168,7 @@ suites = [ 'debugger.sh', 'extra-sandbox-profile.sh', 'help.sh', + 'symlinks.sh', ], 'workdir': meson.current_source_dir(), }, @@ -244,8 +247,6 @@ foreach suite : suites # Used for target dependency/ordering tracking, not adding compiler flags or anything. depends : suite['deps'], workdir : workdir, - # Won't pass until man pages are generated - should_fail : suite['name'] == 'main' and script == 'help.sh' ) endforeach endforeach diff --git a/tests/functional/misc.sh b/tests/functional/misc.sh index 7d63756b7f4..cb4d4139f4c 100755 --- a/tests/functional/misc.sh +++ b/tests/functional/misc.sh @@ -11,7 +11,7 @@ source common.sh #nix-hash --help | grepQuiet base32 # Can we ask for the version number? -nix-env --version | grep "$version" +nix-env --version | grep -F "${_NIX_TEST_CLIENT_VERSION:-$version}" nix_env=$(type -P nix-env) (PATH=""; ! $nix_env --help 2>&1 ) | grepQuiet -F "The 'man' command was not found, but it is needed for 'nix-env' and some other 'nix-*' commands' help text. Perhaps you could install the 'man' command?" diff --git a/tests/functional/multiple-outputs.nix b/tests/functional/multiple-outputs.nix index 6ba7c523d8e..2c9243097d5 100644 --- a/tests/functional/multiple-outputs.nix +++ b/tests/functional/multiple-outputs.nix @@ -5,94 +5,111 @@ rec { # Want to ensure that "out" doesn't get a suffix on it's path. nameCheck = mkDerivation { name = "multiple-outputs-a"; - outputs = [ "out" "dev" ]; - builder = builtins.toFile "builder.sh" - '' - mkdir $first $second - test -z $all - echo "first" > $first/file - echo "second" > $second/file - ln -s $first $second/link - ''; + outputs = [ + "out" + "dev" + ]; + builder = builtins.toFile "builder.sh" '' + mkdir $first $second + test -z $all + echo "first" > $first/file + echo "second" > $second/file + ln -s $first $second/link + ''; helloString = "Hello, world!"; }; a = mkDerivation { name = "multiple-outputs-a"; - outputs = [ "first" "second" ]; - builder = builtins.toFile "builder.sh" - '' - mkdir $first $second - test -z $all - echo "first" > $first/file - echo "second" > $second/file - ln -s $first $second/link - ''; + outputs = [ + "first" + "second" + ]; + builder = builtins.toFile "builder.sh" '' + mkdir $first $second + test -z $all + echo "first" > $first/file + echo "second" > $second/file + ln -s $first $second/link + ''; helloString = "Hello, world!"; }; use-a = mkDerivation { name = "use-a"; inherit (a) first second; - builder = builtins.toFile "builder.sh" - '' - cat $first/file $second/file >$out - ''; + builder = builtins.toFile "builder.sh" '' + cat $first/file $second/file >$out + ''; }; b = mkDerivation { - defaultOutput = assert a.second.helloString == "Hello, world!"; a; - firstOutput = assert a.outputName == "first"; a.first.first; - secondOutput = assert a.second.outputName == "second"; a.second.first.first.second.second.first.second; + defaultOutput = + assert a.second.helloString == "Hello, world!"; + a; + firstOutput = + assert a.outputName == "first"; + a.first.first; + secondOutput = + assert a.second.outputName == "second"; + a.second.first.first.second.second.first.second; allOutputs = a.all; name = "multiple-outputs-b"; - builder = builtins.toFile "builder.sh" - '' - mkdir $out - test "$firstOutput $secondOutput" = "$allOutputs" - test "$defaultOutput" = "$firstOutput" - test "$(cat $firstOutput/file)" = "first" - test "$(cat $secondOutput/file)" = "second" - echo "success" > $out/file - ''; + builder = builtins.toFile "builder.sh" '' + mkdir $out + test "$firstOutput $secondOutput" = "$allOutputs" + test "$defaultOutput" = "$firstOutput" + test "$(cat $firstOutput/file)" = "first" + test "$(cat $secondOutput/file)" = "second" + echo "success" > $out/file + ''; }; c = mkDerivation { name = "multiple-outputs-c"; drv = b.drvPath; - builder = builtins.toFile "builder.sh" - '' - mkdir $out - ln -s $drv $out/drv - ''; + builder = builtins.toFile "builder.sh" '' + mkdir $out + ln -s $drv $out/drv + ''; }; d = mkDerivation { name = "multiple-outputs-d"; drv = builtins.unsafeDiscardOutputDependency b.drvPath; - builder = builtins.toFile "builder.sh" - '' - mkdir $out - echo $drv > $out/drv - ''; + builder = builtins.toFile "builder.sh" '' + mkdir $out + echo $drv > $out/drv + ''; }; - cyclic = (mkDerivation { - name = "cyclic-outputs"; - outputs = [ "a" "b" "c" ]; - builder = builtins.toFile "builder.sh" - '' + cyclic = + (mkDerivation { + name = "cyclic-outputs"; + outputs = [ + "a" + "b" + "c" + ]; + builder = builtins.toFile "builder.sh" '' mkdir $a $b $c echo $a > $b/foo echo $b > $c/bar echo $c > $a/baz ''; - }).a; + }).a; e = mkDerivation { name = "multiple-outputs-e"; - outputs = [ "a_a" "b" "c" ]; - meta.outputsToInstall = [ "a_a" "b" ]; + outputs = [ + "a_a" + "b" + "c" + ]; + meta.outputsToInstall = [ + "a_a" + "b" + ]; buildCommand = "mkdir $a_a $b $c"; }; @@ -104,33 +121,37 @@ rec { independent = mkDerivation { name = "multiple-outputs-independent"; - outputs = [ "first" "second" ]; - builder = builtins.toFile "builder.sh" - '' - mkdir $first $second - test -z $all - echo "first" > $first/file - echo "second" > $second/file - ''; + outputs = [ + "first" + "second" + ]; + builder = builtins.toFile "builder.sh" '' + mkdir $first $second + test -z $all + echo "first" > $first/file + echo "second" > $second/file + ''; }; use-independent = mkDerivation { name = "use-independent"; inherit (a) first second; - builder = builtins.toFile "builder.sh" - '' - cat $first/file $second/file >$out - ''; + builder = builtins.toFile "builder.sh" '' + cat $first/file $second/file >$out + ''; }; invalid-output-name-1 = mkDerivation { name = "invalid-output-name-1"; - outputs = [ "out/"]; + outputs = [ "out/" ]; }; invalid-output-name-2 = mkDerivation { name = "invalid-output-name-2"; - outputs = [ "x" "foo$"]; + outputs = [ + "x" + "foo$" + ]; }; } diff --git a/tests/functional/nar-access.nix b/tests/functional/nar-access.nix index 9948abe59ff..b1e88189a39 100644 --- a/tests/functional/nar-access.nix +++ b/tests/functional/nar-access.nix @@ -1,23 +1,22 @@ with import ./config.nix; rec { - a = mkDerivation { - name = "nar-index-a"; - builder = builtins.toFile "builder.sh" - '' - mkdir $out - mkdir $out/foo - touch $out/foo-x - touch $out/foo/bar - touch $out/foo/baz - touch $out/qux - mkdir $out/zyx + a = mkDerivation { + name = "nar-index-a"; + builder = builtins.toFile "builder.sh" '' + mkdir $out + mkdir $out/foo + touch $out/foo-x + touch $out/foo/bar + touch $out/foo/baz + touch $out/qux + mkdir $out/zyx - cat >$out/foo/data <$out/foo/data < $out - '' else '' - cp -r ${../common} ./common - cp ${../common.sh} ./common.sh - cp ${../config.nix} ./config.nix - cp -r ${./.} ./nested-sandboxing + buildCommand = + '' + set -x + set -eu -o pipefail + '' + + ( + if altitude == 0 then + '' + echo Deep enough! > $out + '' + else + '' + cp -r ${../common} ./common + cp ${../common.sh} ./common.sh + cp ${../config.nix} ./config.nix + cp -r ${./.} ./nested-sandboxing - export PATH=${builtins.getEnv "NIX_BIN_DIR"}:$PATH + export PATH=${builtins.getEnv "NIX_BIN_DIR"}:$PATH - export _NIX_TEST_SOURCE_DIR=$PWD - export _NIX_TEST_BUILD_DIR=$PWD + export _NIX_TEST_SOURCE_DIR=$PWD + export _NIX_TEST_BUILD_DIR=$PWD - source common.sh - source ./nested-sandboxing/command.sh + source common.sh + source ./nested-sandboxing/command.sh - runNixBuild ${storeFun} ${toString altitude} >> $out - ''); + runNixBuild ${storeFun} ${toString altitude} >> $out + '' + ); } diff --git a/tests/functional/nix-channel.sh b/tests/functional/nix-channel.sh index 16d6a135568..d0b772850dd 100755 --- a/tests/functional/nix-channel.sh +++ b/tests/functional/nix-channel.sh @@ -68,4 +68,14 @@ nix-env -i dependencies-top [ -e $TEST_HOME/.nix-profile/foobar ] # Test evaluation through a channel symlink (#9882). -nix-instantiate '' +drvPath=$(nix-instantiate '') + +# Add a test for the special case behaviour of 'nixpkgs' in the +# channels for root (see EvalSettings::getDefaultNixPath()). +if ! isTestOnNixOS; then + nix-channel --add file://$TEST_ROOT/foo nixpkgs + nix-channel --update + mv $TEST_HOME/.local/state/nix/profiles $TEST_ROOT/var/nix/profiles/per-user/root + drvPath2=$(nix-instantiate '') + [[ "$drvPath" = "$drvPath2" ]] +fi diff --git a/tests/functional/nix-profile.sh b/tests/functional/nix-profile.sh index 7cf5fcb7456..b1cfef6b0b2 100755 --- a/tests/functional/nix-profile.sh +++ b/tests/functional/nix-profile.sh @@ -52,7 +52,7 @@ cp "${config_nix}" $flake1Dir/ # Test upgrading from nix-env. nix-env -f ./user-envs.nix -i foo-1.0 nix profile list | grep -A2 'Name:.*foo' | grep 'Store paths:.*foo-1.0' -nix profile install $flake1Dir -L +nix profile add $flake1Dir -L nix profile list | grep -A4 'Name:.*flake1' | grep 'Locked flake URL:.*narHash' [[ $($TEST_HOME/.nix-profile/bin/hello) = "Hello World" ]] [ -e $TEST_HOME/.nix-profile/share/man ] @@ -64,12 +64,12 @@ nix profile diff-closures | grep 'env-manifest.nix: ε → ∅' # Test XDG Base Directories support export NIX_CONFIG="use-xdg-base-directories = true" nix profile remove flake1 2>&1 | grep 'removed 1 packages' -nix profile install $flake1Dir +nix profile add $flake1Dir [[ $($TEST_HOME/.local/state/nix/profile/bin/hello) = "Hello World" ]] unset NIX_CONFIG -# Test conflicting package install. -nix profile install $flake1Dir 2>&1 | grep "warning: 'flake1' is already installed" +# Test conflicting package add. +nix profile add $flake1Dir 2>&1 | grep "warning: 'flake1' is already added" # Test upgrading a package. printf NixOS > $flake1Dir/who @@ -132,16 +132,16 @@ nix profile history | grep 'foo: 1.0 -> ∅' nix profile diff-closures | grep 'Version 3 -> 4' # Test installing a non-flake package. -nix profile install --file ./simple.nix '' +nix profile add --file ./simple.nix '' [[ $(cat $TEST_HOME/.nix-profile/hello) = "Hello World!" ]] nix profile remove simple 2>&1 | grep 'removed 1 packages' -nix profile install $(nix-build --no-out-link ./simple.nix) +nix profile add $(nix-build --no-out-link ./simple.nix) [[ $(cat $TEST_HOME/.nix-profile/hello) = "Hello World!" ]] # Test packages with same name from different sources mkdir $TEST_ROOT/simple-too cp ./simple.nix "${config_nix}" simple.builder.sh $TEST_ROOT/simple-too -nix profile install --file $TEST_ROOT/simple-too/simple.nix '' +nix profile add --file $TEST_ROOT/simple-too/simple.nix '' nix profile list | grep -A4 'Name:.*simple' | grep 'Name:.*simple-1' nix profile remove simple 2>&1 | grep 'removed 1 packages' nix profile remove simple-1 2>&1 | grep 'removed 1 packages' @@ -160,13 +160,13 @@ nix profile history | grep "packages.$system.default: 1.0, 1.0-man -> 3.0, 3.0-m nix profile remove flake1 2>&1 | grep 'removed 1 packages' printf 4.0 > $flake1Dir/version printf Utrecht > $flake1Dir/who -nix profile install $flake1Dir +nix profile add $flake1Dir [[ $($TEST_HOME/.nix-profile/bin/hello) = "Hello Utrecht" ]] [[ $(nix path-info --json $(realpath $TEST_HOME/.nix-profile/bin/hello) | jq -r .[].ca) =~ fixed:r:sha256: ]] # Override the outputs. nix profile remove simple flake1 -nix profile install "$flake1Dir^*" +nix profile add "$flake1Dir^*" [[ $($TEST_HOME/.nix-profile/bin/hello) = "Hello Utrecht" ]] [ -e $TEST_HOME/.nix-profile/share/man ] [ -e $TEST_HOME/.nix-profile/include ] @@ -179,7 +179,7 @@ nix profile upgrade flake1 [ -e $TEST_HOME/.nix-profile/include ] nix profile remove flake1 2>&1 | grep 'removed 1 packages' -nix profile install "$flake1Dir^man" +nix profile add "$flake1Dir^man" (! [ -e $TEST_HOME/.nix-profile/bin/hello ]) [ -e $TEST_HOME/.nix-profile/share/man ] (! [ -e $TEST_HOME/.nix-profile/include ]) @@ -193,9 +193,9 @@ printf World > $flake1Dir/who cp -r $flake1Dir $flake2Dir printf World2 > $flake2Dir/who -nix profile install $flake1Dir +nix profile add $flake1Dir [[ $($TEST_HOME/.nix-profile/bin/hello) = "Hello World" ]] -expect 1 nix profile install $flake2Dir +expect 1 nix profile add $flake2Dir diff -u <( nix --offline profile install $flake2Dir 2>&1 1> /dev/null \ | grep -vE "^warning: " \ @@ -214,31 +214,31 @@ error: An existing package already provides the following file: nix profile remove flake1 - The new package can also be installed next to the existing one by assigning a different priority. + The new package can also be added next to the existing one by assigning a different priority. The conflicting packages have a priority of 5. To prioritise the new package: - nix profile install path:${flake2Dir}#packages.${system}.default --priority 4 + nix profile add path:${flake2Dir}#packages.${system}.default --priority 4 To prioritise the existing package: - nix profile install path:${flake2Dir}#packages.${system}.default --priority 6 + nix profile add path:${flake2Dir}#packages.${system}.default --priority 6 EOF ) [[ $($TEST_HOME/.nix-profile/bin/hello) = "Hello World" ]] -nix profile install $flake2Dir --priority 100 +nix profile add $flake2Dir --priority 100 [[ $($TEST_HOME/.nix-profile/bin/hello) = "Hello World" ]] -nix profile install $flake2Dir --priority 0 +nix profile add $flake2Dir --priority 0 [[ $($TEST_HOME/.nix-profile/bin/hello) = "Hello World2" ]] -# nix profile install $flake1Dir --priority 100 +# nix profile add $flake1Dir --priority 100 # [[ $($TEST_HOME/.nix-profile/bin/hello) = "Hello World" ]] # Ensure that conflicts are handled properly even when the installables aren't # flake references. # Regression test for https://github.com/NixOS/nix/issues/8284 clearProfiles -nix profile install $(nix build $flake1Dir --no-link --print-out-paths) -expect 1 nix profile install --impure --expr "(builtins.getFlake ''$flake2Dir'').packages.$system.default" +nix profile add $(nix build $flake1Dir --no-link --print-out-paths) +expect 1 nix profile add --impure --expr "(builtins.getFlake ''$flake2Dir'').packages.$system.default" # Test upgrading from profile version 2. clearProfiles diff --git a/tests/functional/nix-shell.sh b/tests/functional/nix-shell.sh index b054b7f7519..bc49333b505 100755 --- a/tests/functional/nix-shell.sh +++ b/tests/functional/nix-shell.sh @@ -4,7 +4,7 @@ source common.sh clearStoreIfPossible -if [[ -n ${CONTENT_ADDRESSED:-} ]]; then +if [[ -n ${NIX_TESTS_CA_BY_DEFAULT:-} ]]; then shellDotNix="$PWD/ca-shell.nix" else shellDotNix="$PWD/shell.nix" diff --git a/tests/functional/package.nix b/tests/functional/package.nix index d1582b05d14..43f2f25a200 100644 --- a/tests/functional/package.nix +++ b/tests/functional/package.nix @@ -1,103 +1,108 @@ -{ lib -, stdenv -, mkMesonDerivation +{ + lib, + stdenv, + mkMesonDerivation, -, meson -, ninja -, pkg-config + meson, + ninja, + pkg-config, -, jq -, git -, mercurial -, util-linux + jq, + git, + mercurial, + util-linux, + unixtools, -, nix-store -, nix-expr -, nix-cli + nix-store, + nix-expr, + nix-cli, -, busybox-sandbox-shell ? null + busybox-sandbox-shell ? null, -# Configuration Options + # Configuration Options -, pname ? "nix-functional-tests" -, version + pname ? "nix-functional-tests", + version, -# For running the functional tests against a different pre-built Nix. -, test-daemon ? null + # For running the functional tests against a different pre-built Nix. + test-daemon ? null, }: let inherit (lib) fileset; in -mkMesonDerivation (finalAttrs: { - inherit pname version; - - workDir = ./.; - fileset = fileset.unions [ - ../../scripts/nix-profile.sh.in - ../../.version - ../../tests/functional - ./. - ]; - - # Hack for sake of the dev shell - passthru.externalNativeBuildInputs = [ - meson - ninja - pkg-config - - jq - git - mercurial - ] ++ lib.optionals stdenv.hostPlatform.isLinux [ - # For various sandboxing tests that needs a statically-linked shell, - # etc. - busybox-sandbox-shell - # For Overlay FS tests need `mount`, `umount`, and `unshare`. - # For `script` command (ensuring a TTY) - # TODO use `unixtools` to be precise over which executables instead? - util-linux - ]; - - nativeBuildInputs = finalAttrs.passthru.externalNativeBuildInputs ++ [ - nix-cli - ]; - - buildInputs = [ - nix-store - nix-expr - ]; - - preConfigure = - # "Inline" .version so it's not a symlink, and includes the suffix. - # Do the meson utils, without modification. - '' - chmod u+w ./.version - echo ${version} > ../../../.version - '' - # TEMP hack for Meson before make is gone, where - # `src/nix-functional-tests` is during the transition a symlink and - # not the actual directory directory. - + '' - cd $(readlink -e $PWD) - echo $PWD | grep tests/functional +mkMesonDerivation ( + finalAttrs: + { + inherit pname version; + + workDir = ./.; + fileset = fileset.unions [ + ../../scripts/nix-profile.sh.in + ../../.version + ../../tests/functional + ./. + ]; + + # Hack for sake of the dev shell + passthru.externalNativeBuildInputs = + [ + meson + ninja + pkg-config + + jq + git + mercurial + unixtools.script + ] + ++ lib.optionals stdenv.hostPlatform.isLinux [ + # For various sandboxing tests that needs a statically-linked shell, + # etc. + busybox-sandbox-shell + # For Overlay FS tests need `mount`, `umount`, and `unshare`. + # For `script` command (ensuring a TTY) + # TODO use `unixtools` to be precise over which executables instead? + util-linux + ]; + + nativeBuildInputs = finalAttrs.passthru.externalNativeBuildInputs ++ [ + nix-cli + ]; + + buildInputs = [ + nix-store + nix-expr + ]; + + preConfigure = + # TEMP hack for Meson before make is gone, where + # `src/nix-functional-tests` is during the transition a symlink and + # not the actual directory directory. + '' + cd $(readlink -e $PWD) + echo $PWD | grep tests/functional + ''; + + mesonCheckFlags = [ + "--print-errorlogs" + ]; + + doCheck = true; + + installPhase = '' + mkdir $out ''; - mesonCheckFlags = [ - "--print-errorlogs" - ]; - - doCheck = true; - - installPhase = '' - mkdir $out - ''; - - meta = { - platforms = lib.platforms.unix; - }; - -} // lib.optionalAttrs (test-daemon != null) { - NIX_DAEMON_PACKAGE = test-daemon; -}) + meta = { + platforms = lib.platforms.unix; + }; + + } + // lib.optionalAttrs (test-daemon != null) { + # TODO rename to _NIX_TEST_DAEMON_PACKAGE + NIX_DAEMON_PACKAGE = test-daemon; + _NIX_TEST_CLIENT_VERSION = nix-cli.version; + } +) diff --git a/tests/functional/parallel.nix b/tests/functional/parallel.nix index 23f142059f5..0adfe7d8e53 100644 --- a/tests/functional/parallel.nix +++ b/tests/functional/parallel.nix @@ -1,19 +1,33 @@ -{sleepTime ? 3}: +{ + sleepTime ? 3, +}: with import ./config.nix; let - mkDrv = text: inputs: mkDerivation { - name = "parallel"; - builder = ./parallel.builder.sh; - inherit text inputs shared sleepTime; - }; + mkDrv = + text: inputs: + mkDerivation { + name = "parallel"; + builder = ./parallel.builder.sh; + inherit + text + inputs + shared + sleepTime + ; + }; - a = mkDrv "a" []; - b = mkDrv "b" [a]; - c = mkDrv "c" [a]; - d = mkDrv "d" [a]; - e = mkDrv "e" [b c d]; + a = mkDrv "a" [ ]; + b = mkDrv "b" [ a ]; + c = mkDrv "c" [ a ]; + d = mkDrv "d" [ a ]; + e = mkDrv "e" [ + b + c + d + ]; -in e +in +e diff --git a/tests/functional/path.nix b/tests/functional/path.nix index 883c3c41bb1..b554765e85e 100644 --- a/tests/functional/path.nix +++ b/tests/functional/path.nix @@ -3,12 +3,12 @@ with import ./config.nix; mkDerivation { name = "filter"; builder = builtins.toFile "builder" "ln -s $input $out"; - input = - builtins.path { - path = ((builtins.getEnv "TEST_ROOT") + "/filterin"); - filter = path: type: - type != "symlink" - && baseNameOf path != "foo" - && !((import ./lang/lib.nix).hasSuffix ".bak" (baseNameOf path)); - }; + input = builtins.path { + path = ((builtins.getEnv "TEST_ROOT") + "/filterin"); + filter = + path: type: + type != "symlink" + && baseNameOf path != "foo" + && !((import ./lang/lib.nix).hasSuffix ".bak" (baseNameOf path)); + }; } diff --git a/tests/functional/plugins/meson.build b/tests/functional/plugins/meson.build index 3d6b2f0e1d8..41050ffc16a 100644 --- a/tests/functional/plugins/meson.build +++ b/tests/functional/plugins/meson.build @@ -1,16 +1,9 @@ libplugintest = shared_module( 'plugintest', 'plugintest.cc', - cpp_args : [ - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. - '-include', 'config-util.hh', - '-include', 'config-store.hh', - # '-include', 'config-fetchers.hh', - '-include', 'config-expr.hh', - ], dependencies : [ dependency('nix-expr'), + # hack for trailing newline ], build_by_default : false, ) diff --git a/tests/functional/plugins/plugintest.cc b/tests/functional/plugins/plugintest.cc index 7433ad19008..0b1a01a6e3a 100644 --- a/tests/functional/plugins/plugintest.cc +++ b/tests/functional/plugins/plugintest.cc @@ -1,5 +1,5 @@ -#include "config-global.hh" -#include "primops.hh" +#include "nix/util/config-global.hh" +#include "nix/expr/primops.hh" using namespace nix; diff --git a/tests/functional/pure-eval.sh b/tests/functional/pure-eval.sh index 25038109982..45a65f9ab8f 100755 --- a/tests/functional/pure-eval.sh +++ b/tests/functional/pure-eval.sh @@ -34,3 +34,15 @@ rm -rf $TEST_ROOT/eval-out (! nix eval --store dummy:// --write-to $TEST_ROOT/eval-out --expr '{ "." = "bla"; }') (! nix eval --expr '~/foo') + +expectStderr 0 nix eval --expr "/some/absolute/path" \ + | grepQuiet "/some/absolute/path" + +expectStderr 0 nix eval --expr "/some/absolute/path" --impure \ + | grepQuiet "/some/absolute/path" + +expectStderr 0 nix eval --expr "some/relative/path" \ + | grepQuiet "$PWD/some/relative/path" + +expectStderr 0 nix eval --expr "some/relative/path" --impure \ + | grepQuiet "$PWD/some/relative/path" diff --git a/tests/functional/readfile-context.nix b/tests/functional/readfile-context.nix index 54cd1afd9d3..d9880ca3201 100644 --- a/tests/functional/readfile-context.nix +++ b/tests/functional/readfile-context.nix @@ -25,4 +25,5 @@ let input = builtins.readFile (dependent + "/file1"); }; -in readDependent +in +readDependent diff --git a/tests/functional/recursive.nix b/tests/functional/recursive.nix index fe438f0ba5c..be9e55da37e 100644 --- a/tests/functional/recursive.nix +++ b/tests/functional/recursive.nix @@ -1,4 +1,6 @@ -let config_nix = /. + "${builtins.getEnv "_NIX_TEST_BUILD_DIR"}/config.nix"; in +let + config_nix = /. + "${builtins.getEnv "_NIX_TEST_BUILD_DIR"}/config.nix"; +in with import config_nix; mkDerivation rec { @@ -15,7 +17,9 @@ mkDerivation rec { buildCommand = '' mkdir $out - opts="--experimental-features nix-command ${if (NIX_TESTS_CA_BY_DEFAULT == "1") then "--extra-experimental-features ca-derivations" else ""}" + opts="--experimental-features nix-command ${ + if (NIX_TESTS_CA_BY_DEFAULT == "1") then "--extra-experimental-features ca-derivations" else "" + }" PATH=${builtins.getEnv "NIX_BIN_DIR"}:$PATH diff --git a/tests/functional/repl.sh b/tests/functional/repl.sh index 706e0f5dba9..bfe18c9e586 100755 --- a/tests/functional/repl.sh +++ b/tests/functional/repl.sh @@ -56,6 +56,10 @@ testRepl () { nix repl "${nixArgs[@]}" 2>&1 <<< "builtins.currentSystem" \ | grep "$(nix-instantiate --eval -E 'builtins.currentSystem')" + # regression test for #12163 + replOutput=$(nix repl "${nixArgs[@]}" 2>&1 <<< ":sh import $testDir/simple.nix") + echo "$replOutput" | grepInverse "error: Cannot run 'nix-shell'" + expectStderr 1 nix repl "${testDir}/simple.nix" \ | grepQuiet -s "error: path '$testDir/simple.nix' is not a flake" } @@ -63,7 +67,7 @@ testRepl () { # Simple test, try building a drv testRepl # Same thing (kind-of), but with a remote store. -testRepl --store "$TEST_ROOT/store?real=$NIX_STORE_DIR" +testRepl --store "$TEST_ROOT/other-root?real=$NIX_STORE_DIR" # Remove ANSI escape sequences. They can prevent grep from finding a match. stripColors () { @@ -153,20 +157,64 @@ foo + baz ' "3" \ ./flake ./flake\#bar --experimental-features 'flakes' +testReplResponse $' +:a { a = 1; b = 2; longerName = 3; "with spaces" = 4; } +' 'Added 4 variables. +a, b, longerName, "with spaces" +' + +cat < attribute-set.nix +{ + a = 1; + b = 2; + longerName = 3; + "with spaces" = 4; +} +EOF +testReplResponse ' +:l ./attribute-set.nix +' 'Added 4 variables. +a, b, longerName, "with spaces" +' + +testReplResponseNoRegex $' +:a builtins.foldl\' (x: y: x // y) {} (map (x: { ${builtins.toString x} = x; }) (builtins.genList (x: x) 23)) +' 'Added 23 variables. +"0", "1", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "2", "20", "21", "22", "3", "4", "5", "6" +... and 3 more; view with :ll' + # Test the `:reload` mechansim with flakes: # - Eval `./flake#changingThing` # - Modify the flake # - Re-eval it # - Check that the result has changed -replResult=$( ( -echo "changingThing" -sleep 1 # Leave the repl the time to eval 'foo' +mkfifo repl_fifo +touch repl_output +nix repl ./flake --experimental-features 'flakes' < repl_fifo >> repl_output 2>&1 & +repl_pid=$! +exec 3>repl_fifo # Open fifo for writing +echo "changingThing" >&3 +for i in $(seq 1 1000); do + if grep -q "beforeChange" repl_output; then + break + fi + cat repl_output + sleep 0.1 +done +if [[ "$i" -eq 100 ]]; then + echo "Timed out waiting for beforeChange" + exit 1 +fi + sed -i 's/beforeChange/afterChange/' flake/flake.nix -echo ":reload" -echo "changingThing" -) | nix repl ./flake --experimental-features 'flakes') -echo "$replResult" | grepQuiet -s beforeChange -echo "$replResult" | grepQuiet -s afterChange + +# Send reload and second command +echo ":reload" >&3 +echo "changingThing" >&3 +echo "exit" >&3 +exec 3>&- # Close fifo +wait $repl_pid # Wait for process to finish +grep -q "afterChange" repl_output # Test recursive printing and formatting # Normal output should print attributes in lexicographical order non-recursively @@ -256,6 +304,12 @@ testReplResponseNoRegex ' } ' +# Don't prompt for more input when getting unexpected EOF in imported files. +testReplResponse " +import $testDir/lang/parse-fail-eof-pos.nix +" \ +'.*error: syntax error, unexpected end of file.*' + # TODO: move init to characterisation/framework.sh badDiff=0 badExitCode=0 @@ -301,7 +355,8 @@ runRepl () { -e "s@$testDir@/path/to/tests/functional@g" \ -e "s@$testDirNoUnderscores@/path/to/tests/functional@g" \ -e "s@$nixVersion@@g" \ - -e "s@Added [0-9]* variables@Added variables@g" \ + -e "/Added [0-9]* variables/{s@ [0-9]* @ @;n;d}" \ + -e '/\.\.\. and [0-9]* more; view with :ll/d' \ | grep -vF $'warning: you don\'t have Internet access; disabling some network-dependent features' \ ; } @@ -309,7 +364,7 @@ runRepl () { for test in $(cd "$testDir/repl"; echo *.in); do test="$(basename "$test" .in)" in="$testDir/repl/$test.in" - actual="$testDir/repl/$test.actual" + actual="$TEST_ROOT/$test.actual" expected="$testDir/repl/$test.expected" (cd "$testDir/repl"; set +x; runRepl 2>&1) < "$in" > "$actual" || { echo "FAIL: $test (exit code $?)" >&2 diff --git a/tests/functional/repl/doc-comment-curried-args.expected b/tests/functional/repl/doc-comment-curried-args.expected index 56607e911e8..d2a5bf32853 100644 --- a/tests/functional/repl/doc-comment-curried-args.expected +++ b/tests/functional/repl/doc-comment-curried-args.expected @@ -6,7 +6,8 @@ Added variables. nix-repl> :doc curriedArgs Function `curriedArgs`\ - … defined at /path/to/tests/functional/repl/doc-comments.nix:48:5 + … defined at /path/to/tests/functional/repl/doc-comments.nix:87:5 + A documented function. @@ -17,7 +18,8 @@ nix-repl> "Note that users may not expect this to behave as it currently does" nix-repl> :doc x Function `curriedArgs`\ - … defined at /path/to/tests/functional/repl/doc-comments.nix:50:5 + … defined at /path/to/tests/functional/repl/doc-comments.nix:91:5 + The function returned by applying once diff --git a/tests/functional/repl/doc-comment-formals.expected b/tests/functional/repl/doc-comment-formals.expected index 1024919f4b9..357cf998680 100644 --- a/tests/functional/repl/doc-comment-formals.expected +++ b/tests/functional/repl/doc-comment-formals.expected @@ -9,6 +9,7 @@ nix-repl> "Note that this is not yet complete" nix-repl> :doc documentedFormals Function `documentedFormals`\ - … defined at /path/to/tests/functional/repl/doc-comments.nix:57:5 + … defined at /path/to/tests/functional/repl/doc-comments.nix:104:5 + Finds x diff --git a/tests/functional/repl/doc-comment-function.expected b/tests/functional/repl/doc-comment-function.expected index 3889c4f7860..030cfc3265a 100644 --- a/tests/functional/repl/doc-comment-function.expected +++ b/tests/functional/repl/doc-comment-function.expected @@ -2,6 +2,7 @@ Nix Type :? for help. nix-repl> :doc import ./doc-comment-function.nix -Function defined at /path/to/tests/functional/repl/doc-comment-function.nix:2:1 +Function defined at /path/to/tests/functional/repl/doc-comment-function.nix:4:1 + A doc comment for a file that only contains a function diff --git a/tests/functional/repl/doc-comment-function.nix b/tests/functional/repl/doc-comment-function.nix index cdd2413476f..a85d4a99fdb 100644 --- a/tests/functional/repl/doc-comment-function.nix +++ b/tests/functional/repl/doc-comment-function.nix @@ -1,3 +1,4 @@ -/** A doc comment for a file that only contains a function */ -{ ... }: -{ } +/** + A doc comment for a file that only contains a function +*/ +{ ... }: { } diff --git a/tests/functional/repl/doc-comments.nix b/tests/functional/repl/doc-comments.nix index e91ee0b513d..a7a285d48b9 100644 --- a/tests/functional/repl/doc-comments.nix +++ b/tests/functional/repl/doc-comments.nix @@ -6,55 +6,106 @@ multiply 2 3 => 6 ``` - */ + */ multiply = x: y: x * y; - /**👈 precisely this wide 👉*/ + /** + 👈 precisely this wide 👉 + */ measurement = x: x; - floatedIn = /** This also works. */ + floatedIn = + /** + This also works. + */ x: y: x; - compact=/**boom*/x: x; + compact = + /** + boom + */ + x: x; # https://github.com/NixOS/rfcs/blob/master/rfcs/0145-doc-strings.md#ambiguous-placement - /** Ignore!!! */ - unambiguous = - /** Very close */ + /** + Ignore!!! + */ + unambiguous = + /** + Very close + */ x: x; - /** Firmly rigid. */ + /** + Firmly rigid. + */ constant = true; - /** Immovably fixed. */ + /** + Immovably fixed. + */ lib.version = "9000"; - /** Unchangeably constant. */ + /** + Unchangeably constant. + */ lib.attr.empty = { }; lib.attr.undocumented = { }; - nonStrict = /** My syntax is not strict, but I'm strict anyway. */ x: x; - strict = /** I don't have to be strict, but I am anyway. */ { ... }: null; + nonStrict = + /** + My syntax is not strict, but I'm strict anyway. + */ + x: x; + strict = + /** + I don't have to be strict, but I am anyway. + */ + { ... }: null; # Note that pre and post are the same here. I just had to name them somehow. - strictPre = /** Here's one way to do this */ a@{ ... }: a; - strictPost = /** Here's another way to do this */ { ... }@a: a; + strictPre = + /** + Here's one way to do this + */ + a@{ ... }: a; + strictPost = + /** + Here's another way to do this + */ + { ... }@a: a; # TODO - /** You won't see this. */ + /** + You won't see this. + */ curriedArgs = - /** A documented function. */ + /** + A documented function. + */ x: - /** The function returned by applying once */ + /** + The function returned by applying once + */ y: - /** A function body performing summation of two items */ + /** + A function body performing summation of two items + */ x + y; - /** Documented formals (but you won't see this comment) */ + /** + Documented formals (but you won't see this comment) + */ documentedFormals = - /** Finds x */ - { /** The x attribute */ - x - }: x; + /** + Finds x + */ + { + /** + The x attribute + */ + x, + }: + x; } diff --git a/tests/functional/repl/doc-compact.expected b/tests/functional/repl/doc-compact.expected index 79f1fd44f59..276de2e60b5 100644 --- a/tests/functional/repl/doc-compact.expected +++ b/tests/functional/repl/doc-compact.expected @@ -6,6 +6,7 @@ Added variables. nix-repl> :doc compact Function `compact`\ - … defined at /path/to/tests/functional/repl/doc-comments.nix:18:20 + … defined at /path/to/tests/functional/repl/doc-comments.nix:27:5 + boom diff --git a/tests/functional/repl/doc-constant.expected b/tests/functional/repl/doc-constant.expected index 5787e04dc19..a68188b25ab 100644 --- a/tests/functional/repl/doc-constant.expected +++ b/tests/functional/repl/doc-constant.expected @@ -10,25 +10,27 @@ error: value does not have documentation nix-repl> :doc lib.version Attribute `version` - … defined at /path/to/tests/functional/repl/doc-comments.nix:30:3 + … defined at /path/to/tests/functional/repl/doc-comments.nix:47:3 + Immovably fixed. nix-repl> :doc lib.attr.empty Attribute `empty` - … defined at /path/to/tests/functional/repl/doc-comments.nix:33:3 + … defined at /path/to/tests/functional/repl/doc-comments.nix:52:3 + Unchangeably constant. nix-repl> :doc lib.attr.undocument error: … while evaluating the attribute 'attr.undocument' - at /path/to/tests/functional/repl/doc-comments.nix:33:3: - 32| /** Unchangeably constant. */ - 33| lib.attr.empty = { }; + at /path/to/tests/functional/repl/doc-comments.nix:52:3: + 51| */ + 52| lib.attr.empty = { }; | ^ - 34| + 53| error: attribute 'undocument' missing at «string»:1:1: @@ -39,28 +41,31 @@ error: nix-repl> :doc (import ./doc-comments.nix).constant Attribute `constant` - … defined at /path/to/tests/functional/repl/doc-comments.nix:27:3 + … defined at /path/to/tests/functional/repl/doc-comments.nix:42:3 + Firmly rigid. nix-repl> :doc (import ./doc-comments.nix).lib.version Attribute `version` - … defined at /path/to/tests/functional/repl/doc-comments.nix:30:3 + … defined at /path/to/tests/functional/repl/doc-comments.nix:47:3 + Immovably fixed. nix-repl> :doc (import ./doc-comments.nix).lib.attr.empty Attribute `empty` - … defined at /path/to/tests/functional/repl/doc-comments.nix:33:3 + … defined at /path/to/tests/functional/repl/doc-comments.nix:52:3 + Unchangeably constant. nix-repl> :doc (import ./doc-comments.nix).lib.attr.undocumented Attribute `undocumented` - … defined at /path/to/tests/functional/repl/doc-comments.nix:35:3 + … defined at /path/to/tests/functional/repl/doc-comments.nix:54:3 No documentation found. @@ -97,11 +102,11 @@ error: attribute 'missing' missing nix-repl> :doc lib.attr.undocumental error: … while evaluating the attribute 'attr.undocumental' - at /path/to/tests/functional/repl/doc-comments.nix:33:3: - 32| /** Unchangeably constant. */ - 33| lib.attr.empty = { }; + at /path/to/tests/functional/repl/doc-comments.nix:52:3: + 51| */ + 52| lib.attr.empty = { }; | ^ - 34| + 53| error: attribute 'undocumental' missing at «string»:1:1: diff --git a/tests/functional/repl/doc-floatedIn.expected b/tests/functional/repl/doc-floatedIn.expected index 82bb80b9501..3bf1c40715b 100644 --- a/tests/functional/repl/doc-floatedIn.expected +++ b/tests/functional/repl/doc-floatedIn.expected @@ -6,6 +6,7 @@ Added variables. nix-repl> :doc floatedIn Function `floatedIn`\ - … defined at /path/to/tests/functional/repl/doc-comments.nix:16:5 + … defined at /path/to/tests/functional/repl/doc-comments.nix:21:5 + This also works. diff --git a/tests/functional/repl/doc-functor.expected b/tests/functional/repl/doc-functor.expected index 8cb2706ef0f..503fb807368 100644 --- a/tests/functional/repl/doc-functor.expected +++ b/tests/functional/repl/doc-functor.expected @@ -20,7 +20,7 @@ Look, it's just like a function! nix-repl> :doc recursive Function `__functor`\ - … defined at /path/to/tests/functional/repl/doc-functor.nix:77:23 + … defined at /path/to/tests/functional/repl/doc-functor.nix:82:23 This looks bad, but the docs are ok because of the eta expansion. @@ -30,27 +30,27 @@ error: … while partially calling '__functor' to retrieve documentation … while calling '__functor' - at /path/to/tests/functional/repl/doc-functor.nix:85:17: - 84| */ - 85| __functor = self: self.__functor self; + at /path/to/tests/functional/repl/doc-functor.nix:90:17: + 89| */ + 90| __functor = self: self.__functor self; | ^ - 86| }; + 91| }; … from call site - at /path/to/tests/functional/repl/doc-functor.nix:85:23: - 84| */ - 85| __functor = self: self.__functor self; + at /path/to/tests/functional/repl/doc-functor.nix:90:23: + 89| */ + 90| __functor = self: self.__functor self; | ^ - 86| }; + 91| }; (19999 duplicate frames omitted) error: stack overflow; max-call-depth exceeded - at /path/to/tests/functional/repl/doc-functor.nix:85:23: - 84| */ - 85| __functor = self: self.__functor self; + at /path/to/tests/functional/repl/doc-functor.nix:90:23: + 89| */ + 90| __functor = self: self.__functor self; | ^ - 86| }; + 91| }; nix-repl> :doc diverging error: @@ -59,18 +59,18 @@ error: (10000 duplicate frames omitted) … while calling '__functor' - at /path/to/tests/functional/repl/doc-functor.nix:97:19: - 96| f = x: { - 97| __functor = self: (f (x + 1)); - | ^ - 98| }; + at /path/to/tests/functional/repl/doc-functor.nix:103:21: + 102| f = x: { + 103| __functor = self: (f (x + 1)); + | ^ + 104| }; error: stack overflow; max-call-depth exceeded - at /path/to/tests/functional/repl/doc-functor.nix:97:26: - 96| f = x: { - 97| __functor = self: (f (x + 1)); - | ^ - 98| }; + at /path/to/tests/functional/repl/doc-functor.nix:103:28: + 102| f = x: { + 103| __functor = self: (f (x + 1)); + | ^ + 104| }; nix-repl> :doc helper Function `square`\ @@ -81,21 +81,21 @@ Compute x^2 nix-repl> :doc helper2 Function `__functor`\ - … defined at /path/to/tests/functional/repl/doc-functor.nix:45:23 + … defined at /path/to/tests/functional/repl/doc-functor.nix:46:13 This is a function that can be overridden. nix-repl> :doc lib.helper3 Function `__functor`\ - … defined at /path/to/tests/functional/repl/doc-functor.nix:45:23 + … defined at /path/to/tests/functional/repl/doc-functor.nix:46:13 This is a function that can be overridden. nix-repl> :doc helper3 Function `__functor`\ - … defined at /path/to/tests/functional/repl/doc-functor.nix:45:23 + … defined at /path/to/tests/functional/repl/doc-functor.nix:46:13 This is a function that can be overridden. diff --git a/tests/functional/repl/doc-functor.nix b/tests/functional/repl/doc-functor.nix index f526f453f19..8a663886cf2 100644 --- a/tests/functional/repl/doc-functor.nix +++ b/tests/functional/repl/doc-functor.nix @@ -25,14 +25,14 @@ rec { makeOverridable = f: { /** This is a function that can be overridden. - */ + */ __functor = self: f; override = throw "not implemented"; }; /** Compute x^2 - */ + */ square = x: x * x; helper = makeOverridable square; @@ -41,8 +41,14 @@ rec { makeVeryOverridable = f: { /** This is a function that can be overridden. - */ - __functor = self: arg: f arg // { override = throw "not implemented"; overrideAttrs = throw "not implemented"; }; + */ + __functor = + self: arg: + f arg + // { + override = throw "not implemented"; + overrideAttrs = throw "not implemented"; + }; override = throw "not implemented"; }; @@ -64,7 +70,6 @@ rec { */ helper3 = makeVeryOverridable (x: x * x * x); - # ------ # getDoc traverses a potentially infinite structure in case of __functor, so @@ -73,7 +78,7 @@ rec { recursive = { /** This looks bad, but the docs are ok because of the eta expansion. - */ + */ __functor = self: x: self x; }; @@ -81,21 +86,23 @@ rec { /** Docs probably won't work in this case, because the "partial" application of self results in an infinite recursion. - */ + */ __functor = self: self.__functor self; }; - diverging = let - /** - Docs probably won't work in this case, because the "partial" application - of self results in an diverging computation that causes a stack overflow. - It's not an infinite recursion because each call is different. - This must be handled by the documentation retrieval logic, as it - reimplements the __functor invocation to be partial. - */ - f = x: { - __functor = self: (f (x + 1)); - }; - in f null; + diverging = + let + /** + Docs probably won't work in this case, because the "partial" application + of self results in an diverging computation that causes a stack overflow. + It's not an infinite recursion because each call is different. + This must be handled by the documentation retrieval logic, as it + reimplements the __functor invocation to be partial. + */ + f = x: { + __functor = self: (f (x + 1)); + }; + in + f null; } diff --git a/tests/functional/repl/doc-lambda-flavors.expected b/tests/functional/repl/doc-lambda-flavors.expected index ab5c956390f..437c09d2b31 100644 --- a/tests/functional/repl/doc-lambda-flavors.expected +++ b/tests/functional/repl/doc-lambda-flavors.expected @@ -6,24 +6,28 @@ Added variables. nix-repl> :doc nonStrict Function `nonStrict`\ - … defined at /path/to/tests/functional/repl/doc-comments.nix:37:70 + … defined at /path/to/tests/functional/repl/doc-comments.nix:60:5 + My syntax is not strict, but I'm strict anyway. nix-repl> :doc strict Function `strict`\ - … defined at /path/to/tests/functional/repl/doc-comments.nix:38:63 + … defined at /path/to/tests/functional/repl/doc-comments.nix:65:5 + I don't have to be strict, but I am anyway. nix-repl> :doc strictPre Function `strictPre`\ - … defined at /path/to/tests/functional/repl/doc-comments.nix:40:48 + … defined at /path/to/tests/functional/repl/doc-comments.nix:71:5 + Here's one way to do this nix-repl> :doc strictPost Function `strictPost`\ - … defined at /path/to/tests/functional/repl/doc-comments.nix:41:53 + … defined at /path/to/tests/functional/repl/doc-comments.nix:76:5 + Here's another way to do this diff --git a/tests/functional/repl/doc-measurement.expected b/tests/functional/repl/doc-measurement.expected index 555cac9a2a0..862697613be 100644 --- a/tests/functional/repl/doc-measurement.expected +++ b/tests/functional/repl/doc-measurement.expected @@ -6,6 +6,7 @@ Added variables. nix-repl> :doc measurement Function `measurement`\ - … defined at /path/to/tests/functional/repl/doc-comments.nix:13:17 + … defined at /path/to/tests/functional/repl/doc-comments.nix:15:17 + 👈 precisely this wide 👉 diff --git a/tests/functional/repl/doc-unambiguous.expected b/tests/functional/repl/doc-unambiguous.expected index 0db5505d781..32ca9aef22a 100644 --- a/tests/functional/repl/doc-unambiguous.expected +++ b/tests/functional/repl/doc-unambiguous.expected @@ -6,6 +6,7 @@ Added variables. nix-repl> :doc unambiguous Function `unambiguous`\ - … defined at /path/to/tests/functional/repl/doc-comments.nix:24:5 + … defined at /path/to/tests/functional/repl/doc-comments.nix:37:5 + Very close diff --git a/tests/functional/restricted.sh b/tests/functional/restricted.sh index a92a9b8a3a2..00ee4ddc8c2 100755 --- a/tests/functional/restricted.sh +++ b/tests/functional/restricted.sh @@ -23,7 +23,7 @@ nix-instantiate --restrict-eval ./simple.nix -I src1=./simple.nix -I src2=./conf (! nix-instantiate --restrict-eval --eval -E 'builtins.readFile ./simple.nix') nix-instantiate --restrict-eval --eval -E 'builtins.readFile ./simple.nix' -I src=../.. -expectStderr 1 nix-instantiate --restrict-eval --eval -E 'let __nixPath = [ { prefix = "foo"; path = ./.; } ]; in builtins.readFile ' | grepQuiet "was not found in the Nix search path" +expectStderr 1 nix-instantiate --restrict-eval --eval -E 'let __nixPath = [ { prefix = "foo"; path = ./.; } ]; in builtins.readFile ' | grepQuiet "forbidden in restricted mode" nix-instantiate --restrict-eval --eval -E 'let __nixPath = [ { prefix = "foo"; path = ./.; } ]; in builtins.readFile ' -I src=. p=$(nix eval --raw --expr "builtins.fetchurl file://${_NIX_TEST_SOURCE_DIR}/restricted.sh" --impure --restrict-eval --allowed-uris "file://${_NIX_TEST_SOURCE_DIR}") diff --git a/tests/functional/secure-drv-outputs.nix b/tests/functional/secure-drv-outputs.nix index b4ac8ff531f..169c3c5875b 100644 --- a/tests/functional/secure-drv-outputs.nix +++ b/tests/functional/secure-drv-outputs.nix @@ -4,20 +4,18 @@ with import ./config.nix; good = mkDerivation { name = "good"; - builder = builtins.toFile "builder" - '' - mkdir $out - echo > $out/good - ''; + builder = builtins.toFile "builder" '' + mkdir $out + echo > $out/good + ''; }; bad = mkDerivation { name = "good"; - builder = builtins.toFile "builder" - '' - mkdir $out - echo > $out/bad - ''; + builder = builtins.toFile "builder" '' + mkdir $out + echo > $out/bad + ''; }; } diff --git a/tests/functional/shell-hello.nix b/tests/functional/shell-hello.nix index c920d7cb459..470798dd9e1 100644 --- a/tests/functional/shell-hello.nix +++ b/tests/functional/shell-hello.nix @@ -3,57 +3,56 @@ with import ./config.nix; rec { hello = mkDerivation { name = "hello"; - outputs = [ "out" "dev" ]; + outputs = [ + "out" + "dev" + ]; meta.outputsToInstall = [ "out" ]; - buildCommand = - '' - mkdir -p $out/bin $dev/bin + buildCommand = '' + mkdir -p $out/bin $dev/bin - cat > $out/bin/hello < $out/bin/hello < $dev/bin/hello2 < $dev/bin/hello2 < $out/bin/hello < $out/bin/hello < $out/bin/env <&2 - exit 1 - fi - exec env - EOF - chmod +x $out/bin/env - ''; + cat > $out/bin/env <&2 + exit 1 + fi + exec env + EOF + chmod +x $out/bin/env + ''; }; } diff --git a/tests/functional/shell.nix b/tests/functional/shell.nix index 4b1a0623a81..5e9f4881819 100644 --- a/tests/functional/shell.nix +++ b/tests/functional/shell.nix @@ -1,102 +1,130 @@ -{ inNixShell ? false, contentAddressed ? false, fooContents ? "foo" }: +{ + inNixShell ? false, + contentAddressed ? false, + fooContents ? "foo", +}: -let cfg = import ./config.nix; in +let + cfg = import ./config.nix; +in with cfg; let mkDerivation = if contentAddressed then - args: cfg.mkDerivation ({ - __contentAddressed = true; - outputHashMode = "recursive"; - outputHashAlgo = "sha256"; - } // args) - else cfg.mkDerivation; + args: + cfg.mkDerivation ( + { + __contentAddressed = true; + outputHashMode = "recursive"; + outputHashAlgo = "sha256"; + } + // args + ) + else + cfg.mkDerivation; in -let pkgs = rec { - setupSh = builtins.toFile "setup" '' - export VAR_FROM_STDENV_SETUP=foo - for pkg in $buildInputs; do - export PATH=$PATH:$pkg/bin - done - - declare -a arr1=(1 2 "3 4" 5) - declare -a arr2=(x $'\n' $'x\ny') - fun() { - echo blabla - } - runHook() { - eval "''${!1}" - } - ''; +let + pkgs = rec { + setupSh = builtins.toFile "setup" '' + export VAR_FROM_STDENV_SETUP=foo + for pkg in $buildInputs; do + export PATH=$PATH:$pkg/bin + done - stdenv = mkDerivation { - name = "stdenv"; - buildCommand = '' - mkdir -p $out - ln -s ${setupSh} $out/setup + declare -a arr1=(1 2 "3 4" 5) + declare -a arr2=(x $'\n' $'x\ny') + fun() { + echo blabla + } + runHook() { + eval "''${!1}" + } ''; - } // { inherit mkDerivation; }; - shellDrv = mkDerivation { - name = "shellDrv"; - builder = "/does/not/exist"; - VAR_FROM_NIX = "bar"; - ASCII_PERCENT = "%"; - ASCII_AT = "@"; - TEST_inNixShell = if inNixShell then "true" else "false"; - FOO = fooContents; - inherit stdenv; - outputs = ["dev" "out"]; - } // { - shellHook = abort "Ignore non-drv shellHook attr"; - }; + stdenv = + mkDerivation { + name = "stdenv"; + buildCommand = '' + mkdir -p $out + ln -s ${setupSh} $out/setup + ''; + } + // { + inherit mkDerivation; + }; - # https://github.com/NixOS/nix/issues/5431 - # See nix-shell.sh - polo = mkDerivation { - name = "polo"; - inherit stdenv; - shellHook = '' - echo Polo - ''; - }; + shellDrv = + mkDerivation { + name = "shellDrv"; + builder = "/does/not/exist"; + VAR_FROM_NIX = "bar"; + ASCII_PERCENT = "%"; + ASCII_AT = "@"; + TEST_inNixShell = if inNixShell then "true" else "false"; + FOO = fooContents; + inherit stdenv; + outputs = [ + "dev" + "out" + ]; + } + // { + shellHook = abort "Ignore non-drv shellHook attr"; + }; - # Used by nix-shell -p - runCommand = name: args: buildCommand: mkDerivation (args // { - inherit name buildCommand stdenv; - }); + # https://github.com/NixOS/nix/issues/5431 + # See nix-shell.sh + polo = mkDerivation { + name = "polo"; + inherit stdenv; + shellHook = '' + echo Polo + ''; + }; - foo = runCommand "foo" {} '' - mkdir -p $out/bin - echo 'echo ${fooContents}' > $out/bin/foo - chmod a+rx $out/bin/foo - ln -s ${shell} $out/bin/bash - ''; + # Used by nix-shell -p + runCommand = + name: args: buildCommand: + mkDerivation ( + args + // { + inherit name buildCommand stdenv; + } + ); - bar = runCommand "bar" {} '' - mkdir -p $out/bin - echo 'echo bar' > $out/bin/bar - chmod a+rx $out/bin/bar - ''; + foo = runCommand "foo" { } '' + mkdir -p $out/bin + echo 'echo ${fooContents}' > $out/bin/foo + chmod a+rx $out/bin/foo + ln -s ${shell} $out/bin/bash + ''; - bash = shell; - bashInteractive = runCommand "bash" {} '' - mkdir -p $out/bin - ln -s ${shell} $out/bin/bash - ''; + bar = runCommand "bar" { } '' + mkdir -p $out/bin + echo 'echo bar' > $out/bin/bar + chmod a+rx $out/bin/bar + ''; - # ruby "interpreter" that outputs "$@" - ruby = runCommand "ruby" {} '' - mkdir -p $out/bin - echo 'printf %s "$*"' > $out/bin/ruby - chmod a+rx $out/bin/ruby - ''; + bash = shell; + bashInteractive = runCommand "bash" { } '' + mkdir -p $out/bin + ln -s ${shell} $out/bin/bash + ''; - inherit (cfg) shell; + # ruby "interpreter" that outputs "$@" + ruby = runCommand "ruby" { } '' + mkdir -p $out/bin + echo 'printf %s "$*"' > $out/bin/ruby + chmod a+rx $out/bin/ruby + ''; - callPackage = f: args: f (pkgs // args); + inherit (cfg) shell; - inherit pkgs; -}; in pkgs + callPackage = f: args: f (pkgs // args); + + inherit pkgs; + }; +in +pkgs diff --git a/tests/functional/shell.sh b/tests/functional/shell.sh index cfc8e410284..51032ff1b75 100755 --- a/tests/functional/shell.sh +++ b/tests/functional/shell.sh @@ -52,6 +52,7 @@ if isDaemonNewer "2.20.0pre20231220"; then fi requireSandboxSupport +requiresUnprivilegedUserNamespaces chmod -R u+w "$TEST_ROOT/store0" || true rm -rf "$TEST_ROOT/store0" diff --git a/tests/functional/signing.sh b/tests/functional/signing.sh index 8ec093a4803..2893efec7d9 100755 --- a/tests/functional/signing.sh +++ b/tests/functional/signing.sh @@ -110,3 +110,13 @@ nix store verify --store "$TEST_ROOT"/store0 -r "$outPath2" --trusted-public-key # Content-addressed stuff can be copied without signatures. nix copy --to "$TEST_ROOT"/store0 "$outPathCA" + +# Test multiple signing keys +nix copy --to "file://$TEST_ROOT/storemultisig?secret-keys=$TEST_ROOT/sk1,$TEST_ROOT/sk2" "$outPath" +for file in "$TEST_ROOT/storemultisig/"*.narinfo; do + if [[ "$(grep -cE '^Sig: cache[1,2]\.example.org' "$file")" -ne 2 ]]; then + echo "ERROR: Cannot find cache1.example.org and cache2.example.org signatures in ${file}" + cat "${file}" + exit 1 + fi +done diff --git a/tests/functional/simple-failing.nix b/tests/functional/simple-failing.nix index d176c9c51e6..6cf29ae3842 100644 --- a/tests/functional/simple-failing.nix +++ b/tests/functional/simple-failing.nix @@ -2,11 +2,10 @@ with import ./config.nix; mkDerivation { name = "simple-failing"; - builder = builtins.toFile "builder.sh" - '' - echo "This should fail" - exit 1 - ''; + builder = builtins.toFile "builder.sh" '' + echo "This should fail" + exit 1 + ''; PATH = ""; goodPath = path; } diff --git a/tests/functional/simple.sh b/tests/functional/simple.sh index 8afa369c2e2..c1f2eef411e 100755 --- a/tests/functional/simple.sh +++ b/tests/functional/simple.sh @@ -15,7 +15,7 @@ echo "output path is $outPath" [[ ! -w $outPath ]] text=$(cat "$outPath/hello") -if test "$text" != "Hello World!"; then exit 1; fi +[[ "$text" = "Hello World!" ]] TODO_NixOS diff --git a/tests/functional/store-info.sh b/tests/functional/store-info.sh index beecc2dd9dc..b1e0772b5ae 100755 --- a/tests/functional/store-info.sh +++ b/tests/functional/store-info.sh @@ -3,9 +3,11 @@ source common.sh STORE_INFO=$(nix store info 2>&1) +LEGACY_STORE_INFO=$(nix store ping 2>&1) # alias to nix store info STORE_INFO_JSON=$(nix store info --json) echo "$STORE_INFO" | grep "Store URL: ${NIX_REMOTE}" +echo "$LEGACY_STORE_INFO" | grep "Store URL: ${NIX_REMOTE}" if [[ -v NIX_DAEMON_PACKAGE ]] && isDaemonNewer "2.7.0pre20220126"; then DAEMON_VERSION=$("$NIX_DAEMON_PACKAGE"/bin/nix daemon --version | cut -d' ' -f3) @@ -13,6 +15,7 @@ if [[ -v NIX_DAEMON_PACKAGE ]] && isDaemonNewer "2.7.0pre20220126"; then [[ "$(echo "$STORE_INFO_JSON" | jq -r ".version")" == "$DAEMON_VERSION" ]] fi + expect 127 NIX_REMOTE=unix:"$PWD"/store nix store info || \ fail "nix store info on a non-existent store should fail" diff --git a/tests/functional/structured-attrs-shell.nix b/tests/functional/structured-attrs-shell.nix index 57c1e6bd2da..e9b9f1e3937 100644 --- a/tests/functional/structured-attrs-shell.nix +++ b/tests/functional/structured-attrs-shell.nix @@ -12,9 +12,16 @@ mkDerivation { name = "structured2"; __structuredAttrs = true; inherit stdenv; - outputs = [ "out" "dev" ]; - my.list = [ "a" "b" "c" ]; - exportReferencesGraph.refs = [ dep ]; + outputs = [ + "out" + "dev" + ]; + my.list = [ + "a" + "b" + "c" + ]; + exportReferencesGraph.refs = dep; buildCommand = '' touch ''${outputs[out]}; touch ''${outputs[dev]} ''; diff --git a/tests/functional/structured-attrs.nix b/tests/functional/structured-attrs.nix index e93139a4457..4e19845176e 100644 --- a/tests/functional/structured-attrs.nix +++ b/tests/functional/structured-attrs.nix @@ -16,7 +16,10 @@ mkDerivation { __structuredAttrs = true; - outputs = [ "out" "dev" ]; + outputs = [ + "out" + "dev" + ]; buildCommand = '' set -x @@ -43,12 +46,24 @@ mkDerivation { [[ $json =~ '"references":[]' ]] ''; - buildInputs = [ "a" "b" "c" 123 "'" "\"" null ]; + buildInputs = [ + "a" + "b" + "c" + 123 + "'" + "\"" + null + ]; hardening.format = true; hardening.fortify = false; - outer.inner = [ 1 2 3 ]; + outer.inner = [ + 1 + 2 + 3 + ]; int = 123456789; diff --git a/tests/functional/structured-attrs.sh b/tests/functional/structured-attrs.sh index 64d136e993a..2bd9b4aaf1b 100755 --- a/tests/functional/structured-attrs.sh +++ b/tests/functional/structured-attrs.sh @@ -40,3 +40,14 @@ jsonOut="$(nix print-dev-env -f structured-attrs-shell.nix --json)" test "$(<<<"$jsonOut" jq '.structuredAttrs|keys|.[]' -r)" = "$(printf ".attrs.json\n.attrs.sh")" test "$(<<<"$jsonOut" jq '.variables.outputs.value.out' -r)" = "$(<<<"$jsonOut" jq '.structuredAttrs.".attrs.json"' -r | jq -r '.outputs.out')" + +# Hacky way of making structured attrs. We should preserve for now for back compat, but also deprecate. + +hackyExpr='derivation { name = "a"; system = "foo"; builder = "/bin/sh"; __json = builtins.toJSON { a = 1; }; }' + +# Check for deprecation message +expectStderr 0 nix-instantiate --expr "$hackyExpr" --eval --strict | grepQuiet "In derivation 'a': setting structured attributes via '__json' is deprecated, and may be disallowed in future versions of Nix. Set '__structuredAttrs = true' instead." + +# Check it works with the expected structured attrs +hacky=$(nix-instantiate --expr "$hackyExpr") +nix derivation show "$hacky" | jq --exit-status '."'"$hacky"'".structuredAttrs | . == {"a": 1}' diff --git a/tests/functional/supplementary-groups.sh b/tests/functional/supplementary-groups.sh index 50259a3e1f9..a667d3e998c 100755 --- a/tests/functional/supplementary-groups.sh +++ b/tests/functional/supplementary-groups.sh @@ -9,12 +9,11 @@ needLocalStore "The test uses --store always so we would just be bypassing the d TODO_NixOS -unshare --mount --map-root-user -- bash -e -x < using namespace nix; diff --git a/tests/functional/test-libstoreconsumer/meson.build b/tests/functional/test-libstoreconsumer/meson.build index 7076127f70a..ce566035fee 100644 --- a/tests/functional/test-libstoreconsumer/meson.build +++ b/tests/functional/test-libstoreconsumer/meson.build @@ -1,14 +1,9 @@ libstoreconsumer_tester = executable( 'test-libstoreconsumer', 'main.cc', - cpp_args : [ - # TODO(Qyriad): Yes this is how the autoconf+Make system did it. - # It would be nice for our headers to be idempotent instead. - '-include', 'config-util.hh', - '-include', 'config-store.hh', - ], dependencies : [ dependency('nix-store'), + # hack for trailing newline ], build_by_default : false, ) diff --git a/tests/functional/undefined-variable.nix b/tests/functional/undefined-variable.nix index 579985497e9..8e88dd8fe02 100644 --- a/tests/functional/undefined-variable.nix +++ b/tests/functional/undefined-variable.nix @@ -1 +1,4 @@ -let f = builtins.toFile "test-file.nix" "asd"; in import f +let + f = builtins.toFile "test-file.nix" "asd"; +in +import f diff --git a/tests/functional/user-envs-test-case.sh b/tests/functional/user-envs-test-case.sh index 117c6c7a4bc..3483a4600d7 100644 --- a/tests/functional/user-envs-test-case.sh +++ b/tests/functional/user-envs-test-case.sh @@ -173,13 +173,21 @@ nix-env -q '*' | grepQuiet bar-0.1.1 # Test priorities: foo-0.1 has a lower priority than foo-1.0, so it # should be possible to install both without a collision. Also test -# ‘--set-flag priority’ to manually override the declared priorities. +# '-i --priority' and '--set-flag priority' to manually override the +# declared priorities. nix-env -e '*' nix-env -i foo-0.1 foo-1.0 [ "$($profiles/test/bin/foo)" = "foo-1.0" ] nix-env --set-flag priority 1 foo-0.1 [ "$($profiles/test/bin/foo)" = "foo-0.1" ] +# Priorities can be overridden with the --priority flag +nix-env -e '*' +nix-env -i foo-1.0 +[ "$($profiles/test/bin/foo)" = "foo-1.0" ] +nix-env -i --priority 1 foo-0.1 +[ "$($profiles/test/bin/foo)" = "foo-0.1" ] + # Test nix-env --set. nix-env --set $outPath10 [ "$(nix-store -q --resolve $profiles/test)" = $outPath10 ] diff --git a/tests/functional/user-envs.nix b/tests/functional/user-envs.nix index 46f8b51dda1..cc63812c4a7 100644 --- a/tests/functional/user-envs.nix +++ b/tests/functional/user-envs.nix @@ -1,5 +1,6 @@ # Some dummy arguments... -{ foo ? "foo" +{ + foo ? "foo", }: with import ./config.nix; @@ -8,27 +9,41 @@ assert foo == "foo"; let - platforms = let x = "foobar"; in [ x x ]; + platforms = + let + x = "foobar"; + in + [ + x + x + ]; - makeDrv = name: progName: (mkDerivation { - name = assert progName != "fail"; name; - inherit progName system; - builder = ./user-envs.builder.sh; - } // { - meta = { - description = "A silly test package with some \${escaped anti-quotation} in it"; - inherit platforms; - }; - }); + makeDrv = + name: progName: + ( + mkDerivation { + name = + assert progName != "fail"; + name; + inherit progName system; + builder = ./user-envs.builder.sh; + } + // { + meta = { + description = "A silly test package with some \${escaped anti-quotation} in it"; + inherit platforms; + }; + } + ); in - [ - (makeDrv "foo-1.0" "foo") - (makeDrv "foo-2.0pre1" "foo") - (makeDrv "bar-0.1" "bar") - (makeDrv "foo-2.0" "foo") - (makeDrv "bar-0.1.1" "bar") - (makeDrv "foo-0.1" "foo" // { meta.priority = 10; }) - (makeDrv "fail-0.1" "fail") - ] +[ + (makeDrv "foo-1.0" "foo") + (makeDrv "foo-2.0pre1" "foo") + (makeDrv "bar-0.1" "bar") + (makeDrv "foo-2.0" "foo") + (makeDrv "bar-0.1.1" "bar") + (makeDrv "foo-0.1" "foo" // { meta.priority = 10; }) + (makeDrv "fail-0.1" "fail") +] diff --git a/tests/installer/default.nix b/tests/installer/default.nix index 4aed6eae489..d48537dd0d0 100644 --- a/tests/installer/default.nix +++ b/tests/installer/default.nix @@ -1,5 +1,6 @@ -{ binaryTarballs -, nixpkgsFor +{ + binaryTarballs, + nixpkgsFor, }: let @@ -41,8 +42,9 @@ let }; }; - mockChannel = pkgs: - pkgs.runCommandNoCC "mock-channel" {} '' + mockChannel = + pkgs: + pkgs.runCommandNoCC "mock-channel" { } '' mkdir nixexprs mkdir -p $out/channel echo -n 'someContent' > nixexprs/someFile @@ -54,14 +56,14 @@ let images = { /* - "ubuntu-14-04" = { - image = import { - url = "https://app.vagrantup.com/ubuntu/boxes/trusty64/versions/20190514.0.0/providers/virtualbox.box"; - hash = "sha256-iUUXyRY8iW7DGirb0zwGgf1fRbLA7wimTJKgP7l/OQ8="; + "ubuntu-14-04" = { + image = import { + url = "https://app.vagrantup.com/ubuntu/boxes/trusty64/versions/20190514.0.0/providers/virtualbox.box"; + hash = "sha256-iUUXyRY8iW7DGirb0zwGgf1fRbLA7wimTJKgP7l/OQ8="; + }; + rootDisk = "box-disk1.vmdk"; + system = "x86_64-linux"; }; - rootDisk = "box-disk1.vmdk"; - system = "x86_64-linux"; - }; */ "ubuntu-16-04" = { @@ -95,14 +97,14 @@ let # Currently fails with 'error while loading shared libraries: # libsodium.so.23: cannot stat shared object: Invalid argument'. /* - "rhel-6" = { - image = import { - url = "https://app.vagrantup.com/generic/boxes/rhel6/versions/4.1.12/providers/libvirt.box"; - hash = "sha256-QwzbvRoRRGqUCQptM7X/InRWFSP2sqwRt2HaaO6zBGM="; + "rhel-6" = { + image = import { + url = "https://app.vagrantup.com/generic/boxes/rhel6/versions/4.1.12/providers/libvirt.box"; + hash = "sha256-QwzbvRoRRGqUCQptM7X/InRWFSP2sqwRt2HaaO6zBGM="; + }; + rootDisk = "box.img"; + system = "x86_64-linux"; }; - rootDisk = "box.img"; - system = "x86_64-linux"; - }; */ "rhel-7" = { @@ -137,12 +139,18 @@ let }; - makeTest = imageName: testName: - let image = images.${imageName}; in + makeTest = + imageName: testName: + let + image = images.${imageName}; + in with nixpkgsFor.${image.system}.native; - runCommand - "installer-test-${imageName}-${testName}" - { buildInputs = [ qemu_kvm openssh ]; + runCommand "installer-test-${imageName}-${testName}" + { + buildInputs = [ + qemu_kvm + openssh + ]; image = image.image; postBoot = image.postBoot or ""; installScript = installScripts.${testName}.script; @@ -247,9 +255,6 @@ let in -builtins.mapAttrs (imageName: image: - { ${image.system} = builtins.mapAttrs (testName: test: - makeTest imageName testName - ) installScripts; - } -) images +builtins.mapAttrs (imageName: image: { + ${image.system} = builtins.mapAttrs (testName: test: makeTest imageName testName) installScripts; +}) images diff --git a/tests/nixos/authorization.nix b/tests/nixos/authorization.nix index fdeae06ed34..6540e9fa337 100644 --- a/tests/nixos/authorization.nix +++ b/tests/nixos/authorization.nix @@ -4,8 +4,11 @@ nodes.machine = { virtualisation.writableStore = true; # TODO add a test without allowed-users setting. allowed-users is uncommon among NixOS users. - nix.settings.allowed-users = ["alice" "bob"]; - nix.settings.trusted-users = ["alice"]; + nix.settings.allowed-users = [ + "alice" + "bob" + ]; + nix.settings.trusted-users = [ "alice" ]; users.users.alice.isNormalUser = true; users.users.bob.isNormalUser = true; @@ -15,80 +18,80 @@ }; testScript = - let - pathFour = "/nix/store/20xfy868aiic0r0flgzq4n5dq1yvmxkn-four"; - in - '' - machine.wait_for_unit("multi-user.target") - machine.succeed(""" - exec 1>&2 - echo kSELDhobKaF8/VdxIxdP7EQe+Q > one - diff $(nix store add-file one) one - """) - machine.succeed(""" - su --login alice -c ' - set -x - cd ~ - echo ehHtmfuULXYyBV6NBk6QUi8iE0 > two - ls - diff $(echo $(nix store add-file two)) two' 1>&2 - """) - machine.succeed(""" - su --login bob -c ' - set -x - cd ~ - echo 0Jw8RNp7cK0W2AdNbcquofcOVk > three - diff $(nix store add-file three) three - ' 1>&2 - """) + let + pathFour = "/nix/store/20xfy868aiic0r0flgzq4n5dq1yvmxkn-four"; + in + '' + machine.wait_for_unit("multi-user.target") + machine.succeed(""" + exec 1>&2 + echo kSELDhobKaF8/VdxIxdP7EQe+Q > one + diff $(nix store add-file one) one + """) + machine.succeed(""" + su --login alice -c ' + set -x + cd ~ + echo ehHtmfuULXYyBV6NBk6QUi8iE0 > two + ls + diff $(echo $(nix store add-file two)) two' 1>&2 + """) + machine.succeed(""" + su --login bob -c ' + set -x + cd ~ + echo 0Jw8RNp7cK0W2AdNbcquofcOVk > three + diff $(nix store add-file three) three + ' 1>&2 + """) - # We're going to check that a path is not created - machine.succeed(""" - ! [[ -e ${pathFour} ]] - """) - machine.succeed(""" - su --login mallory -c ' - set -x - cd ~ - echo 5mgtDj0ohrWkT50TLR0f4tIIxY > four; - (! nix store add-file four 2>&1) | grep -F "cannot open connection to remote store" - (! nix store add-file four 2>&1) | grep -F "Connection reset by peer" + # We're going to check that a path is not created + machine.succeed(""" ! [[ -e ${pathFour} ]] - ' 1>&2 - """) - - # Check that the file _can_ be added, and matches the expected path we were checking - machine.succeed(""" - exec 1>&2 - echo 5mgtDj0ohrWkT50TLR0f4tIIxY > four - four="$(nix store add-file four)" - diff $four four - diff <(echo $four) <(echo ${pathFour}) - """) + """) + machine.succeed(""" + su --login mallory -c ' + set -x + cd ~ + echo 5mgtDj0ohrWkT50TLR0f4tIIxY > four; + (! nix store add-file four 2>&1) | grep -F "cannot open connection to remote store" + (! nix store add-file four 2>&1) | grep -F "Connection reset by peer" + ! [[ -e ${pathFour} ]] + ' 1>&2 + """) - machine.succeed(""" - su --login alice -c 'nix-store --verify --repair' - """) + # Check that the file _can_ be added, and matches the expected path we were checking + machine.succeed(""" + exec 1>&2 + echo 5mgtDj0ohrWkT50TLR0f4tIIxY > four + four="$(nix store add-file four)" + diff $four four + diff <(echo $four) <(echo ${pathFour}) + """) - machine.succeed(""" - set -x - su --login bob -c '(! nix-store --verify --repair 2>&1)' | tee diag 1>&2 - grep -F "you are not privileged to repair paths" diag - """) + machine.succeed(""" + su --login alice -c 'nix-store --verify --repair' + """) - machine.succeed(""" + machine.succeed(""" set -x - su --login mallory -c ' - nix-store --generate-binary-cache-key cache1.example.org sk1 pk1 - (! nix store sign --key-file sk1 ${pathFour} 2>&1)' | tee diag 1>&2 - grep -F "cannot open connection to remote store 'daemon'" diag - """) + su --login bob -c '(! nix-store --verify --repair 2>&1)' | tee diag 1>&2 + grep -F "you are not privileged to repair paths" diag + """) - machine.succeed(""" - su --login bob -c ' - nix-store --generate-binary-cache-key cache1.example.org sk1 pk1 - nix store sign --key-file sk1 ${pathFour} - ' - """) - ''; + machine.succeed(""" + set -x + su --login mallory -c ' + nix-store --generate-binary-cache-key cache1.example.org sk1 pk1 + (! nix store sign --key-file sk1 ${pathFour} 2>&1)' | tee diag 1>&2 + grep -F "cannot open connection to remote store 'daemon'" diag + """) + + machine.succeed(""" + su --login bob -c ' + nix-store --generate-binary-cache-key cache1.example.org sk1 pk1 + nix store sign --key-file sk1 ${pathFour} + ' + """) + ''; } diff --git a/tests/nixos/ca-fd-leak/default.nix b/tests/nixos/ca-fd-leak/default.nix index a6ae72adc93..902aacdc650 100644 --- a/tests/nixos/ca-fd-leak/default.nix +++ b/tests/nixos/ca-fd-leak/default.nix @@ -27,12 +27,15 @@ let # domain socket. # Compiled statically so that we can easily send it to the VM and use it # inside the build sandbox. - sender = pkgs.runCommandWith { - name = "sender"; - stdenv = pkgs.pkgsStatic.stdenv; - } '' - $CC -static -o $out ${./sender.c} - ''; + sender = + pkgs.runCommandWith + { + name = "sender"; + stdenv = pkgs.pkgsStatic.stdenv; + } + '' + $CC -static -o $out ${./sender.c} + ''; # Okay, so we have a file descriptor shipped out of the FOD now. But the # Nix store is read-only, right? .. Well, yeah. But this file descriptor @@ -47,44 +50,57 @@ in name = "ca-fd-leak"; nodes.machine = - { config, lib, pkgs, ... }: - { virtualisation.writableStore = true; + { + config, + lib, + pkgs, + ... + }: + { + virtualisation.writableStore = true; nix.settings.substituters = lib.mkForce [ ]; - virtualisation.additionalPaths = [ pkgs.busybox-sandbox-shell sender smuggler pkgs.socat ]; + virtualisation.additionalPaths = [ + pkgs.busybox-sandbox-shell + sender + smuggler + pkgs.socat + ]; }; - testScript = { nodes }: '' - start_all() + testScript = + { nodes }: + '' + start_all() - machine.succeed("echo hello") - # Start the smuggler server - machine.succeed("${smuggler}/bin/smuggler ${socketName} >&2 &") + machine.succeed("echo hello") + # Start the smuggler server + machine.succeed("${smuggler}/bin/smuggler ${socketName} >&2 &") - # Build the smuggled derivation. - # This will connect to the smuggler server and send it the file descriptor - machine.succeed(r""" - nix-build -E ' - builtins.derivation { - name = "smuggled"; - system = builtins.currentSystem; - # look ma, no tricks! - outputHashMode = "flat"; - outputHashAlgo = "sha256"; - outputHash = builtins.hashString "sha256" "hello, world\n"; - builder = "${pkgs.busybox-sandbox-shell}/bin/sh"; - args = [ "-c" "echo \"hello, world\" > $out; ''${${sender}} ${socketName}" ]; - }' - """.strip()) + # Build the smuggled derivation. + # This will connect to the smuggler server and send it the file descriptor + machine.succeed(r""" + nix-build -E ' + builtins.derivation { + name = "smuggled"; + system = builtins.currentSystem; + # look ma, no tricks! + outputHashMode = "flat"; + outputHashAlgo = "sha256"; + outputHash = builtins.hashString "sha256" "hello, world\n"; + builder = "${pkgs.busybox-sandbox-shell}/bin/sh"; + args = [ "-c" "echo \"hello, world\" > $out; ''${${sender}} ${socketName}" ]; + }' + """.strip()) - # Tell the smuggler server that we're done - machine.execute("echo done | ${pkgs.socat}/bin/socat - ABSTRACT-CONNECT:${socketName}") + # Tell the smuggler server that we're done + machine.execute("echo done | ${pkgs.socat}/bin/socat - ABSTRACT-CONNECT:${socketName}") - # Check that the file was not modified - machine.succeed(r""" - cat ./result - test "$(cat ./result)" = "hello, world" - """.strip()) - ''; + # Check that the file was not modified + machine.succeed(r""" + cat ./result + test "$(cat ./result)" = "hello, world" + """.strip()) + ''; } diff --git a/tests/nixos/ca-fd-leak/sender.c b/tests/nixos/ca-fd-leak/sender.c index 75e54fc8f52..2ec79947a62 100644 --- a/tests/nixos/ca-fd-leak/sender.c +++ b/tests/nixos/ca-fd-leak/sender.c @@ -19,7 +19,7 @@ int main(int argc, char **argv) { struct sockaddr_un data; data.sun_family = AF_UNIX; data.sun_path[0] = 0; - strcpy(data.sun_path + 1, argv[1]); + strncpy(data.sun_path + 1, argv[1], sizeof(data.sun_path) - 2); // Now try to connect, To ensure we work no matter what order we are // executed in, just busyloop here. @@ -49,8 +49,8 @@ int main(int argc, char **argv) { msg.msg_controllen = CMSG_SPACE(sizeof(int)); // Write a single null byte too. - msg.msg_iov = malloc(sizeof(struct iovec)); - msg.msg_iov[0].iov_base = ""; + msg.msg_iov = (struct iovec*) malloc(sizeof(struct iovec)); + msg.msg_iov[0].iov_base = (void*) ""; msg.msg_iov[0].iov_len = 1; msg.msg_iovlen = 1; diff --git a/tests/nixos/ca-fd-leak/smuggler.c b/tests/nixos/ca-fd-leak/smuggler.c index 82acf37e68e..7279c48bf7d 100644 --- a/tests/nixos/ca-fd-leak/smuggler.c +++ b/tests/nixos/ca-fd-leak/smuggler.c @@ -5,6 +5,7 @@ #include #include #include +#include int main(int argc, char **argv) { @@ -16,7 +17,7 @@ int main(int argc, char **argv) { struct sockaddr_un data; data.sun_family = AF_UNIX; data.sun_path[0] = 0; - strcpy(data.sun_path + 1, argv[1]); + strncpy(data.sun_path + 1, argv[1], sizeof(data.sun_path) - 1); int res = bind(sock, (const struct sockaddr *)&data, offsetof(struct sockaddr_un, sun_path) + strlen(argv[1]) @@ -57,10 +58,11 @@ int main(int argc, char **argv) { // Wait for a second connection, which will tell us that the build is // done a = accept(sock, 0, 0); + if (a < 0) perror("accept"); fprintf(stderr, "%s\n", "Got a second connection, rewriting the file"); // Write a new content to the file if (ftruncate(smuggling_fd, 0)) perror("ftruncate"); - char * new_content = "Pwned\n"; + const char * new_content = "Pwned\n"; int written_bytes = write(smuggling_fd, new_content, strlen(new_content)); if (written_bytes != strlen(new_content)) perror("write"); } diff --git a/tests/nixos/cgroups/default.nix b/tests/nixos/cgroups/default.nix index b8febbf4bda..a6b4bca8c76 100644 --- a/tests/nixos/cgroups/default.nix +++ b/tests/nixos/cgroups/default.nix @@ -3,38 +3,39 @@ { name = "cgroups"; - nodes = - { - host = - { config, pkgs, ... }: - { virtualisation.additionalPaths = [ pkgs.stdenvNoCC ]; - nix.extraOptions = - '' - extra-experimental-features = nix-command auto-allocate-uids cgroups - extra-system-features = uid-range - ''; - nix.settings.use-cgroups = true; - nix.nixPath = [ "nixpkgs=${nixpkgs}" ]; - }; - }; - - testScript = { nodes }: '' - start_all() - - host.wait_for_unit("multi-user.target") - - # Start build in background - host.execute("NIX_REMOTE=daemon nix build --auto-allocate-uids --file ${./hang.nix} >&2 &") - service = "/sys/fs/cgroup/system.slice/nix-daemon.service" - - # Wait for cgroups to be created - host.succeed(f"until [ -e {service}/nix-daemon ]; do sleep 1; done", timeout=30) - host.succeed(f"until [ -e {service}/nix-build-uid-* ]; do sleep 1; done", timeout=30) - - # Check that there aren't processes where there shouldn't be, and that there are where there should be - host.succeed(f'[ -z "$(cat {service}/cgroup.procs)" ]') - host.succeed(f'[ -n "$(cat {service}/nix-daemon/cgroup.procs)" ]') - host.succeed(f'[ -n "$(cat {service}/nix-build-uid-*/cgroup.procs)" ]') - ''; + nodes = { + host = + { config, pkgs, ... }: + { + virtualisation.additionalPaths = [ pkgs.stdenvNoCC ]; + nix.extraOptions = '' + extra-experimental-features = nix-command auto-allocate-uids cgroups + extra-system-features = uid-range + ''; + nix.settings.use-cgroups = true; + nix.nixPath = [ "nixpkgs=${nixpkgs}" ]; + }; + }; + + testScript = + { nodes }: + '' + start_all() + + host.wait_for_unit("multi-user.target") + + # Start build in background + host.execute("NIX_REMOTE=daemon nix build --auto-allocate-uids --file ${./hang.nix} >&2 &") + service = "/sys/fs/cgroup/system.slice/nix-daemon.service" + + # Wait for cgroups to be created + host.succeed(f"until [ -e {service}/nix-daemon ]; do sleep 1; done", timeout=30) + host.succeed(f"until [ -e {service}/nix-build-uid-* ]; do sleep 1; done", timeout=30) + + # Check that there aren't processes where there shouldn't be, and that there are where there should be + host.succeed(f'[ -z "$(cat {service}/cgroup.procs)" ]') + host.succeed(f'[ -n "$(cat {service}/nix-daemon/cgroup.procs)" ]') + host.succeed(f'[ -n "$(cat {service}/nix-build-uid-*/cgroup.procs)" ]') + ''; } diff --git a/tests/nixos/cgroups/hang.nix b/tests/nixos/cgroups/hang.nix index cefe2d031c0..d7b337b0c05 100644 --- a/tests/nixos/cgroups/hang.nix +++ b/tests/nixos/cgroups/hang.nix @@ -1,9 +1,10 @@ { }: -with import {}; +with import { }; runCommand "hang" - { requiredSystemFeatures = "uid-range"; + { + requiredSystemFeatures = "uid-range"; } '' sleep infinity diff --git a/tests/nixos/chroot-store.nix b/tests/nixos/chroot-store.nix index 4b167fc3839..0a4fff99222 100644 --- a/tests/nixos/chroot-store.nix +++ b/tests/nixos/chroot-store.nix @@ -1,31 +1,49 @@ -{ lib, config, nixpkgs, ... }: +{ + lib, + config, + nixpkgs, + ... +}: let pkgs = config.nodes.machine.nixpkgs.pkgs; pkgA = pkgs.hello; pkgB = pkgs.cowsay; -in { +in +{ name = "chroot-store"; - nodes = - { machine = - { config, lib, pkgs, ... }: - { virtualisation.writableStore = true; - virtualisation.additionalPaths = [ pkgA ]; - environment.systemPackages = [ pkgB ]; - nix.extraOptions = "experimental-features = nix-command"; - }; - }; + nodes = { + machine = + { + config, + lib, + pkgs, + ... + }: + { + virtualisation.writableStore = true; + virtualisation.additionalPaths = [ pkgA ]; + environment.systemPackages = [ pkgB ]; + nix.extraOptions = "experimental-features = nix-command"; + }; + }; - testScript = { nodes }: '' - # fmt: off - start_all() + testScript = + { nodes }: + '' + # fmt: off + start_all() - machine.succeed("nix copy --no-check-sigs --to /tmp/nix ${pkgA}") + machine.succeed("nix copy --no-check-sigs --to /tmp/nix ${pkgA}") - machine.succeed("nix shell --store /tmp/nix ${pkgA} --command hello >&2") + machine.succeed("nix shell --store /tmp/nix ${pkgA} --command hello >&2") - # Test that /nix/store is available via an overlayfs mount. - machine.succeed("nix shell --store /tmp/nix ${pkgA} --command cowsay foo >&2") - ''; + # Test that /nix/store is available via an overlayfs mount. + machine.succeed("nix shell --store /tmp/nix ${pkgA} --command cowsay foo >&2") + + # Building in /tmp should fail for security reasons. + err = machine.fail("nix build --offline --store /tmp/nix --expr 'builtins.derivation { name = \"foo\"; system = \"x86_64-linux\"; builder = \"/foo\"; }' 2>&1") + assert "is world-writable" in err + ''; } diff --git a/tests/nixos/containers/containers.nix b/tests/nixos/containers/containers.nix index 6773f5628a3..b590dc8498f 100644 --- a/tests/nixos/containers/containers.nix +++ b/tests/nixos/containers/containers.nix @@ -4,60 +4,67 @@ { name = "containers"; - nodes = - { - host = - { config, lib, pkgs, nodes, ... }: - { virtualisation.writableStore = true; - virtualisation.diskSize = 2048; - virtualisation.additionalPaths = - [ pkgs.stdenvNoCC - (import ./systemd-nspawn.nix { inherit nixpkgs; }).toplevel - ]; - virtualisation.memorySize = 4096; - nix.settings.substituters = lib.mkForce [ ]; - nix.extraOptions = - '' - extra-experimental-features = nix-command auto-allocate-uids cgroups - extra-system-features = uid-range - ''; - nix.nixPath = [ "nixpkgs=${nixpkgs}" ]; - }; - }; - - testScript = { nodes }: '' - start_all() - - host.succeed("nix --version >&2") - - # Test that 'id' gives the expected result in various configurations. - - # Existing UIDs, sandbox. - host.succeed("nix build --no-auto-allocate-uids --sandbox -L --offline --impure --file ${./id-test.nix} --argstr name id-test-1") - host.succeed("[[ $(cat ./result) = 'uid=1000(nixbld) gid=100(nixbld) groups=100(nixbld)' ]]") - - # Existing UIDs, no sandbox. - host.succeed("nix build --no-auto-allocate-uids --no-sandbox -L --offline --impure --file ${./id-test.nix} --argstr name id-test-2") - host.succeed("[[ $(cat ./result) = 'uid=30001(nixbld1) gid=30000(nixbld) groups=30000(nixbld)' ]]") - - # Auto-allocated UIDs, sandbox. - host.succeed("nix build --auto-allocate-uids --sandbox -L --offline --impure --file ${./id-test.nix} --argstr name id-test-3") - host.succeed("[[ $(cat ./result) = 'uid=1000(nixbld) gid=100(nixbld) groups=100(nixbld)' ]]") - - # Auto-allocated UIDs, no sandbox. - host.succeed("nix build --auto-allocate-uids --no-sandbox -L --offline --impure --file ${./id-test.nix} --argstr name id-test-4") - host.succeed("[[ $(cat ./result) = 'uid=872415232 gid=30000(nixbld) groups=30000(nixbld)' ]]") - - # Auto-allocated UIDs, UID range, sandbox. - host.succeed("nix build --auto-allocate-uids --sandbox -L --offline --impure --file ${./id-test.nix} --argstr name id-test-5 --arg uidRange true") - host.succeed("[[ $(cat ./result) = 'uid=0(root) gid=0(root) groups=0(root)' ]]") - - # Auto-allocated UIDs, UID range, no sandbox. - host.fail("nix build --auto-allocate-uids --no-sandbox -L --offline --impure --file ${./id-test.nix} --argstr name id-test-6 --arg uidRange true") - - # Run systemd-nspawn in a Nix build. - host.succeed("nix build --auto-allocate-uids --sandbox -L --offline --impure --file ${./systemd-nspawn.nix} --argstr nixpkgs ${nixpkgs}") - host.succeed("[[ $(cat ./result/msg) = 'Hello World' ]]") - ''; + nodes = { + host = + { + config, + lib, + pkgs, + nodes, + ... + }: + { + virtualisation.writableStore = true; + virtualisation.diskSize = 2048; + virtualisation.additionalPaths = [ + pkgs.stdenvNoCC + (import ./systemd-nspawn.nix { inherit nixpkgs; }).toplevel + ]; + virtualisation.memorySize = 4096; + nix.settings.substituters = lib.mkForce [ ]; + nix.extraOptions = '' + extra-experimental-features = nix-command auto-allocate-uids cgroups + extra-system-features = uid-range + ''; + nix.nixPath = [ "nixpkgs=${nixpkgs}" ]; + }; + }; + + testScript = + { nodes }: + '' + start_all() + + host.succeed("nix --version >&2") + + # Test that 'id' gives the expected result in various configurations. + + # Existing UIDs, sandbox. + host.succeed("nix build --no-auto-allocate-uids --sandbox -L --offline --impure --file ${./id-test.nix} --argstr name id-test-1") + host.succeed("[[ $(cat ./result) = 'uid=1000(nixbld) gid=100(nixbld) groups=100(nixbld)' ]]") + + # Existing UIDs, no sandbox. + host.succeed("nix build --no-auto-allocate-uids --no-sandbox -L --offline --impure --file ${./id-test.nix} --argstr name id-test-2") + host.succeed("[[ $(cat ./result) = 'uid=30001(nixbld1) gid=30000(nixbld) groups=30000(nixbld)' ]]") + + # Auto-allocated UIDs, sandbox. + host.succeed("nix build --auto-allocate-uids --sandbox -L --offline --impure --file ${./id-test.nix} --argstr name id-test-3") + host.succeed("[[ $(cat ./result) = 'uid=1000(nixbld) gid=100(nixbld) groups=100(nixbld)' ]]") + + # Auto-allocated UIDs, no sandbox. + host.succeed("nix build --auto-allocate-uids --no-sandbox -L --offline --impure --file ${./id-test.nix} --argstr name id-test-4") + host.succeed("[[ $(cat ./result) = 'uid=872415232 gid=30000(nixbld) groups=30000(nixbld)' ]]") + + # Auto-allocated UIDs, UID range, sandbox. + host.succeed("nix build --auto-allocate-uids --sandbox -L --offline --impure --file ${./id-test.nix} --argstr name id-test-5 --arg uidRange true") + host.succeed("[[ $(cat ./result) = 'uid=0(root) gid=0(root) groups=0(root)' ]]") + + # Auto-allocated UIDs, UID range, no sandbox. + host.fail("nix build --auto-allocate-uids --no-sandbox -L --offline --impure --file ${./id-test.nix} --argstr name id-test-6 --arg uidRange true") + + # Run systemd-nspawn in a Nix build. + host.succeed("nix build --auto-allocate-uids --sandbox -L --offline --impure --file ${./systemd-nspawn.nix} --argstr nixpkgs ${nixpkgs}") + host.succeed("[[ $(cat ./result/msg) = 'Hello World' ]]") + ''; } diff --git a/tests/nixos/containers/id-test.nix b/tests/nixos/containers/id-test.nix index 8eb9d38f9a2..2139327ad88 100644 --- a/tests/nixos/containers/id-test.nix +++ b/tests/nixos/containers/id-test.nix @@ -1,8 +1,10 @@ -{ name, uidRange ? false }: +{ + name, + uidRange ? false, +}: -with import {}; +with import { }; -runCommand name - { requiredSystemFeatures = if uidRange then ["uid-range"] else []; - } - "id; id > $out" +runCommand name { + requiredSystemFeatures = if uidRange then [ "uid-range" ] else [ ]; +} "id; id > $out" diff --git a/tests/nixos/containers/systemd-nspawn.nix b/tests/nixos/containers/systemd-nspawn.nix index 1dad4ebd754..4516f4e1394 100644 --- a/tests/nixos/containers/systemd-nspawn.nix +++ b/tests/nixos/containers/systemd-nspawn.nix @@ -2,7 +2,8 @@ let - machine = { config, pkgs, ... }: + machine = + { config, pkgs, ... }: { system.stateVersion = "22.05"; boot.isContainer = true; @@ -31,10 +32,12 @@ let }; }; - cfg = (import (nixpkgs + "/nixos/lib/eval-config.nix") { - modules = [ machine ]; - system = "x86_64-linux"; - }); + cfg = ( + import (nixpkgs + "/nixos/lib/eval-config.nix") { + modules = [ machine ]; + system = "x86_64-linux"; + } + ); config = cfg.config; @@ -43,7 +46,8 @@ in with cfg._module.args.pkgs; runCommand "test" - { buildInputs = [ config.system.path ]; + { + buildInputs = [ config.system.path ]; requiredSystemFeatures = [ "uid-range" ]; toplevel = config.system.build.toplevel; } diff --git a/tests/nixos/default.nix b/tests/nixos/default.nix index c5f4a23aa08..f0b1a886565 100644 --- a/tests/nixos/default.nix +++ b/tests/nixos/default.nix @@ -1,17 +1,26 @@ -{ lib, nixpkgs, nixpkgsFor, self }: +{ + lib, + nixpkgs, + nixpkgsFor, + nixpkgs-23-11, +}: let nixos-lib = import (nixpkgs + "/nixos/lib") { }; - noTests = pkg: pkg.overrideAttrs ( - finalAttrs: prevAttrs: { - doCheck = false; - doInstallCheck = false; - }); + noTests = + pkg: + pkg.overrideAttrs ( + finalAttrs: prevAttrs: { + doCheck = false; + doInstallCheck = false; + } + ); # https://nixos.org/manual/nixos/unstable/index.html#sec-calling-nixos-tests - runNixOSTestFor = system: test: + runNixOSTestFor = + system: test: (nixos-lib.runTest { imports = [ test @@ -22,7 +31,12 @@ let nixpkgs.pkgs = nixpkgsFor.${system}.native; nix.checkAllErrors = false; # TODO: decide which packaging stage to use. `nix-cli` is efficient, but not the same as the user-facing `everything.nix` package (`default`). Perhaps a good compromise is `everything.nix` + `noTests` defined above? - nix.package = nixpkgsFor.${system}.native.nixComponents.nix-cli; + nix.package = nixpkgsFor.${system}.native.nixComponents2.nix-cli; + + # Evaluate VMs faster + documentation.enable = false; + # this links against nix and might break with our git version. + system.tools.nixos-option.enable = false; }; _module.args.nixpkgs = nixpkgs; _module.args.system = system; @@ -31,44 +45,54 @@ let # allow running tests against older nix versions via `nix eval --apply` # Example: # nix build "$(nix eval --raw --impure .#hydraJobs.tests.fetch-git --apply 't: (t.forNix "2.19.2").drvPath')^*" - forNix = nixVersion: runNixOSTestFor system { - imports = [test]; - defaults.nixpkgs.overlays = [(curr: prev: { - nix = let - packages = (builtins.getFlake "nix/${nixVersion}").packages.${system}; - in packages.nix-cli or packages.nix; - })]; - }; + forNix = + nixVersion: + runNixOSTestFor system { + imports = [ test ]; + defaults.nixpkgs.overlays = [ + (curr: prev: { + nix = + let + packages = (builtins.getFlake "nix/${nixVersion}").packages.${system}; + in + packages.nix-cli or packages.nix; + }) + ]; + }; }; # Checks that a NixOS configuration does not contain any references to our # locally defined Nix version. - checkOverrideNixVersion = { pkgs, lib, ... }: { - # pkgs.nix: The new Nix in this repo - # We disallow it, to make sure we don't accidentally use it. - system.forbiddenDependenciesRegexes = [ - (lib.strings.escapeRegex "nix-${pkgs.nix.version}") - ]; - }; - - otherNixes.nix_2_3.setNixPackage = { lib, pkgs, ... }: { - imports = [ checkOverrideNixVersion ]; - nix.package = lib.mkForce pkgs.nixVersions.nix_2_3; - }; - - otherNixes.nix_2_13.setNixPackage = { lib, pkgs, ... }: { - imports = [ checkOverrideNixVersion ]; - nix.package = lib.mkForce ( - self.inputs.nixpkgs-23-11.legacyPackages.${pkgs.stdenv.hostPlatform.system}.nixVersions.nix_2_13.overrideAttrs (o: { - meta = o.meta // { knownVulnerabilities = []; }; - }) - ); - }; + checkOverrideNixVersion = + { pkgs, lib, ... }: + { + # pkgs.nix: The new Nix in this repo + # We disallow it, to make sure we don't accidentally use it. + system.forbiddenDependenciesRegexes = [ + (lib.strings.escapeRegex "nix-${pkgs.nix.version}") + ]; + }; - otherNixes.nix_2_18.setNixPackage = { lib, pkgs, ... }: { - imports = [ checkOverrideNixVersion ]; - nix.package = lib.mkForce pkgs.nixVersions.nix_2_18; - }; + otherNixes.nix_2_3.setNixPackage = + { lib, pkgs, ... }: + { + imports = [ checkOverrideNixVersion ]; + nix.package = lib.mkForce pkgs.nixVersions.nix_2_3; + }; + + otherNixes.nix_2_13.setNixPackage = + { lib, pkgs, ... }: + { + imports = [ checkOverrideNixVersion ]; + nix.package = lib.mkForce ( + nixpkgs-23-11.legacyPackages.${pkgs.stdenv.hostPlatform.system}.nixVersions.nix_2_13.overrideAttrs + (o: { + meta = o.meta // { + knownVulnerabilities = [ ]; + }; + }) + ); + }; in @@ -81,30 +105,37 @@ in } // lib.concatMapAttrs ( - nixVersion: { setNixPackage, ... }: + nixVersion: + { setNixPackage, ... }: { "remoteBuilds_remote_${nixVersion}" = runNixOSTestFor "x86_64-linux" { name = "remoteBuilds_remote_${nixVersion}"; imports = [ ./remote-builds.nix ]; - builders.config = { lib, pkgs, ... }: { - imports = [ setNixPackage ]; - }; + builders.config = + { lib, pkgs, ... }: + { + imports = [ setNixPackage ]; + }; }; "remoteBuilds_local_${nixVersion}" = runNixOSTestFor "x86_64-linux" { name = "remoteBuilds_local_${nixVersion}"; imports = [ ./remote-builds.nix ]; - nodes.client = { lib, pkgs, ... }: { - imports = [ setNixPackage ]; - }; + nodes.client = + { lib, pkgs, ... }: + { + imports = [ setNixPackage ]; + }; }; "remoteBuildsSshNg_remote_${nixVersion}" = runNixOSTestFor "x86_64-linux" { name = "remoteBuildsSshNg_remote_${nixVersion}"; imports = [ ./remote-builds-ssh-ng.nix ]; - builders.config = { lib, pkgs, ... }: { - imports = [ setNixPackage ]; - }; + builders.config = + { lib, pkgs, ... }: + { + imports = [ setNixPackage ]; + }; }; # FIXME: these tests don't work yet @@ -138,9 +169,7 @@ in containers = runNixOSTestFor "x86_64-linux" ./containers/containers.nix; - setuid = lib.genAttrs - ["x86_64-linux"] - (system: runNixOSTestFor system ./setuid.nix); + setuid = lib.genAttrs [ "x86_64-linux" ] (system: runNixOSTestFor system ./setuid.nix); fetch-git = runNixOSTestFor "x86_64-linux" ./fetch-git; @@ -154,6 +183,8 @@ in functional_root = runNixOSTestFor "x86_64-linux" ./functional/as-root.nix; + functional_symlinked-home = runNixOSTestFor "x86_64-linux" ./functional/symlinked-home.nix; + user-sandboxing = runNixOSTestFor "x86_64-linux" ./user-sandboxing; s3-binary-cache-store = runNixOSTestFor "x86_64-linux" ./s3-binary-cache-store.nix; diff --git a/tests/nixos/fetch-git/default.nix b/tests/nixos/fetch-git/default.nix index 1d6bcb63783..329fb463e8e 100644 --- a/tests/nixos/fetch-git/default.nix +++ b/tests/nixos/fetch-git/default.nix @@ -7,26 +7,27 @@ ]; /* - Test cases + Test cases - Test cases are automatically imported from ./test-cases/{name} + Test cases are automatically imported from ./test-cases/{name} - The following is set up automatically for each test case: - - a repo with the {name} is created on the gitea server - - a repo with the {name} is created on the client - - the client repo is configured to push to the server repo + The following is set up automatically for each test case: + - a repo with the {name} is created on the gitea server + - a repo with the {name} is created on the client + - the client repo is configured to push to the server repo - Python variables: - - repo.path: the path to the directory of the client repo - - repo.git: the git command with the client repo as the working directory - - repo.remote: the url to the server repo + Python variables: + - repo.path: the path to the directory of the client repo + - repo.git: the git command with the client repo as the working directory + - repo.remote: the url to the server repo */ - testCases = - map - (testCaseName: {...}: { + testCases = map ( + testCaseName: + { ... }: + { imports = [ (./test-cases + "/${testCaseName}") ]; # ensures tests are named like their directories they are defined in name = testCaseName; - }) - (lib.attrNames (builtins.readDir ./test-cases)); + } + ) (lib.attrNames (builtins.readDir ./test-cases)); } diff --git a/tests/nixos/fetch-git/test-cases/http-auth/default.nix b/tests/nixos/fetch-git/test-cases/http-auth/default.nix index d483d54fb24..7ad9a8914e2 100644 --- a/tests/nixos/fetch-git/test-cases/http-auth/default.nix +++ b/tests/nixos/fetch-git/test-cases/http-auth/default.nix @@ -5,7 +5,8 @@ script = '' # add a file to the repo client.succeed(f""" - echo ${config.name /* to make the git tree and store path unique */} > {repo.path}/test-case \ + echo ${config.name # to make the git tree and store path unique + } > {repo.path}/test-case \ && echo lutyabrook > {repo.path}/new-york-state \ && {repo.git} add test-case new-york-state \ && {repo.git} commit -m 'commit1' diff --git a/tests/nixos/fetch-git/test-cases/http-simple/default.nix b/tests/nixos/fetch-git/test-cases/http-simple/default.nix index dcab8067e59..51b3882b5a6 100644 --- a/tests/nixos/fetch-git/test-cases/http-simple/default.nix +++ b/tests/nixos/fetch-git/test-cases/http-simple/default.nix @@ -4,7 +4,8 @@ script = '' # add a file to the repo client.succeed(f""" - echo ${config.name /* to make the git tree and store path unique */} > {repo.path}/test-case \ + echo ${config.name # to make the git tree and store path unique + } > {repo.path}/test-case \ && echo chiang-mai > {repo.path}/thailand \ && {repo.git} add test-case thailand \ && {repo.git} commit -m 'commit1' diff --git a/tests/nixos/fetch-git/test-cases/lfs/default.nix b/tests/nixos/fetch-git/test-cases/lfs/default.nix new file mode 100644 index 00000000000..686796fcc6e --- /dev/null +++ b/tests/nixos/fetch-git/test-cases/lfs/default.nix @@ -0,0 +1,228 @@ +{ + # mostly copied from https://github.com/NixOS/nix/blob/358c26fd13a902d9a4032a00e6683571be07a384/tests/nixos/fetch-git/test-cases/fetchTree-shallow/default.nix#L1 + # ty @DavHau + description = "fetchGit smudges LFS pointers if lfs=true"; + script = '' + from tempfile import TemporaryDirectory + + expected_max_size_lfs_pointer = 1024 # 1 KiB (values >= than this cannot be pointers, and test files are 1 MiB) + + # purge nix git cache to make sure we start with a clean slate + client.succeed("rm -rf ~/.cache/nix") + + + with subtest("Request lfs fetch without any .gitattributes file"): + client.succeed(f"dd if=/dev/urandom of={repo.path}/regular bs=1M count=1 >&2") + client.succeed(f"{repo.git} add : >&2") + client.succeed(f"{repo.git} commit -m 'no .gitattributes' >&2") + client.succeed(f"{repo.git} push origin main >&2") + + # memorize the revision + no_gitattributes_rev = client.succeed(f"{repo.git} rev-parse HEAD").strip() + + # fetch with lfs=true, and check that the lack of .gitattributes does not break anything + fetchGit_no_gitattributes_expr = f""" + builtins.fetchGit {{ + url = "{repo.remote}"; + rev = "{no_gitattributes_rev}"; + ref = "main"; + lfs = true; + }} + """ + fetched_no_gitattributes = client.succeed(f""" + nix eval --debug --impure --raw --expr '({fetchGit_no_gitattributes_expr}).outPath' + """) + client.succeed(f"cmp {repo.path}/regular {fetched_no_gitattributes}/regular >&2") + + + with subtest("Add a file that should be tracked by lfs, but isn't"): + # (git lfs cli only throws a warning "Encountered 1 file that should have + # been a pointer, but wasn't") + + client.succeed(f"dd if=/dev/urandom of={repo.path}/black_sheep bs=1M count=1 >&2") + client.succeed(f"echo 'black_sheep filter=lfs -text' >>{repo.path}/.gitattributes") + client.succeed(f"{repo.git} add : >&2") + client.succeed(f"{repo.git} commit -m 'add misleading file' >&2") + client.succeed(f"{repo.git} push origin main >&2") + + # memorize the revision + bad_lfs_rev = client.succeed(f"{repo.git} rev-parse HEAD").strip() + + # test assumption that it can be cloned with regular git first + # (here we see the warning as stated above) + with TemporaryDirectory() as tempdir: + client.succeed(f"git clone -n {repo.remote} {tempdir} >&2") + client.succeed(f"git -C {tempdir} lfs install >&2") + client.succeed(f"git -C {tempdir} checkout {bad_lfs_rev} >&2") + + # check that the file is not a pointer, as expected + file_size_git = client.succeed(f"stat -c %s {tempdir}/black_sheep").strip() + assert int(file_size_git) == 1024 * 1024, \ + f"non lfs file is {file_size_git}b (!= 1MiB), probably a test implementation error" + + lfs_files = client.succeed(f"git -C {tempdir} lfs ls-files").strip() + assert lfs_files == "", "non lfs file is tracked by lfs, probably a test implementation error" + + client.succeed(f"cmp {repo.path}/black_sheep {tempdir}/black_sheep >&2") + + # now fetch without lfs, check that the file is not a pointer + fetchGit_bad_lfs_without_lfs_expr = f""" + builtins.fetchGit {{ + url = "{repo.remote}"; + rev = "{bad_lfs_rev}"; + ref = "main"; + lfs = false; + }} + """ + fetched_bad_lfs_without_lfs = client.succeed(f""" + nix eval --debug --impure --raw --expr '({fetchGit_bad_lfs_without_lfs_expr}).outPath' + """) + + # check that file was not somehow turned into a pointer + file_size_bad_lfs_without_lfs = client.succeed(f"stat -c %s {fetched_bad_lfs_without_lfs}/black_sheep").strip() + + assert int(file_size_bad_lfs_without_lfs) == 1024 * 1024, \ + f"non lfs-enrolled file is {file_size_bad_lfs_without_lfs}b (!= 1MiB), probably a test implementation error" + client.succeed(f"cmp {repo.path}/black_sheep {fetched_bad_lfs_without_lfs}/black_sheep >&2") + + # finally fetch with lfs=true, and check that the bad file does not break anything + fetchGit_bad_lfs_with_lfs_expr = f""" + builtins.fetchGit {{ + url = "{repo.remote}"; + rev = "{bad_lfs_rev}"; + ref = "main"; + lfs = true; + }} + """ + fetchGit_bad_lfs_with_lfs = client.succeed(f""" + nix eval --debug --impure --raw --expr '({fetchGit_bad_lfs_with_lfs_expr}).outPath' + """) + + client.succeed(f"cmp {repo.path}/black_sheep {fetchGit_bad_lfs_with_lfs}/black_sheep >&2") + + + with subtest("Add an lfs-enrolled file to the repo"): + client.succeed(f"dd if=/dev/urandom of={repo.path}/beeg bs=1M count=1 >&2") + client.succeed(f"{repo.git} lfs install >&2") + client.succeed(f"{repo.git} lfs track --filename beeg >&2") + client.succeed(f"{repo.git} add : >&2") + client.succeed(f"{repo.git} commit -m 'add lfs file' >&2") + client.succeed(f"{repo.git} push origin main >&2") + + # memorize the revision + lfs_file_rev = client.succeed(f"{repo.git} rev-parse HEAD").strip() + + # first fetch without lfs, check that we did not smudge the file + fetchGit_nolfs_expr = f""" + builtins.fetchGit {{ + url = "{repo.remote}"; + rev = "{lfs_file_rev}"; + ref = "main"; + lfs = false; + }} + """ + fetched_nolfs = client.succeed(f""" + nix eval --debug --impure --raw --expr '({fetchGit_nolfs_expr}).outPath' + """) + + # check that file was not smudged + file_size_nolfs = client.succeed(f"stat -c %s {fetched_nolfs}/beeg").strip() + + assert int(file_size_nolfs) < expected_max_size_lfs_pointer, \ + f"did not set lfs=true, yet lfs-enrolled file is {file_size_nolfs}b (>= 1KiB), probably smudged when we should not have" + + # now fetch with lfs=true and check that the file was smudged + fetchGit_lfs_expr = f""" + builtins.fetchGit {{ + url = "{repo.remote}"; + rev = "{lfs_file_rev}"; + ref = "main"; + lfs = true; + }} + """ + fetched_lfs = client.succeed(f""" + nix eval --debug --impure --raw --expr '({fetchGit_lfs_expr}).outPath' + """) + + assert fetched_lfs != fetched_nolfs, \ + f"fetching with and without lfs yielded the same store path {fetched_lfs}, fingerprinting error?" + + # check that file was smudged + file_size_lfs = client.succeed(f"stat -c %s {fetched_lfs}/beeg").strip() + assert int(file_size_lfs) == 1024 * 1024, \ + f"set lfs=true, yet lfs-enrolled file is {file_size_lfs}b (!= 1MiB), probably did not smudge when we should have" + + + with subtest("Check that default is lfs=false"): + fetchGit_default_expr = f""" + builtins.fetchGit {{ + url = "{repo.remote}"; + rev = "{lfs_file_rev}"; + ref = "main"; + }} + """ + fetched_default = client.succeed(f""" + nix eval --debug --impure --raw --expr '({fetchGit_default_expr}).outPath' + """) + + # check that file was not smudged + file_size_default = client.succeed(f"stat -c %s {fetched_default}/beeg").strip() + + assert int(file_size_default) < expected_max_size_lfs_pointer, \ + f"did not set lfs, yet lfs-enrolled file is {file_size_default}b (>= 1KiB), probably bad default value" + + with subtest("Use as flake input"): + # May seem reduntant, but this has minor differences compared to raw + # fetchGit which caused failures before + with TemporaryDirectory() as tempdir: + client.succeed(f"mkdir -p {tempdir}") + client.succeed(f""" + printf '{{ + inputs = {{ + foo = {{ + url = "git+{repo.remote}?ref=main&rev={lfs_file_rev}&lfs=1"; + flake = false; + }}; + }}; + outputs = {{ foo, self }}: {{ inherit (foo) outPath; }}; + }}' >{tempdir}/flake.nix + """) + fetched_flake = client.succeed(f""" + nix eval --debug --raw {tempdir}#.outPath + """) + + assert fetched_lfs == fetched_flake, \ + f"fetching as flake input (store path {fetched_flake}) yielded a different result than using fetchGit (store path {fetched_lfs})" + + + with subtest("Check self.lfs"): + client.succeed(f""" + printf '{{ + inputs.self.lfs = true; + outputs = {{ self }}: {{ }}; + }}' >{repo.path}/flake.nix + """) + client.succeed(f"{repo.git} add : >&2") + client.succeed(f"{repo.git} commit -m 'add flake' >&2") + client.succeed(f"{repo.git} push origin main >&2") + + # memorize the revision + self_lfs_rev = client.succeed(f"{repo.git} rev-parse HEAD").strip() + + with TemporaryDirectory() as tempdir: + client.succeed(f"mkdir -p {tempdir}") + client.succeed(f""" + printf '{{ + inputs.foo = {{ + url = "git+{repo.remote}?ref=main&rev={self_lfs_rev}"; + }}; + outputs = {{ foo, self }}: {{ inherit (foo) outPath; }}; + }}' >{tempdir}/flake.nix + """) + fetched_self_lfs = client.succeed(f""" + nix eval --debug --raw {tempdir}#.outPath + """) + + client.succeed(f"cmp {repo.path}/beeg {fetched_self_lfs}/beeg >&2") + ''; +} diff --git a/tests/nixos/fetch-git/test-cases/ssh-simple/default.nix b/tests/nixos/fetch-git/test-cases/ssh-simple/default.nix index f5fba169846..89285d00ed4 100644 --- a/tests/nixos/fetch-git/test-cases/ssh-simple/default.nix +++ b/tests/nixos/fetch-git/test-cases/ssh-simple/default.nix @@ -4,7 +4,8 @@ script = '' # add a file to the repo client.succeed(f""" - echo ${config.name /* to make the git tree and store path unique */} > {repo.path}/test-case \ + echo ${config.name # to make the git tree and store path unique + } > {repo.path}/test-case \ && echo chiang-mai > {repo.path}/thailand \ && {repo.git} add test-case thailand \ && {repo.git} commit -m 'commit1' diff --git a/tests/nixos/fetch-git/testsupport/gitea-repo.nix b/tests/nixos/fetch-git/testsupport/gitea-repo.nix index e9f4adcc1d3..c8244207fbb 100644 --- a/tests/nixos/fetch-git/testsupport/gitea-repo.nix +++ b/tests/nixos/fetch-git/testsupport/gitea-repo.nix @@ -8,25 +8,27 @@ let boolPyLiteral = b: if b then "True" else "False"; - testCaseExtension = { config, ... }: { - options = { - repo.enable = mkOption { - type = types.bool; - default = true; - description = "Whether to provide a repo variable - automatic repo creation."; + testCaseExtension = + { config, ... }: + { + options = { + repo.enable = mkOption { + type = types.bool; + default = true; + description = "Whether to provide a repo variable - automatic repo creation."; + }; + repo.private = mkOption { + type = types.bool; + default = false; + description = "Whether the repo should be private."; + }; }; - repo.private = mkOption { - type = types.bool; - default = false; - description = "Whether the repo should be private."; + config = mkIf config.repo.enable { + setupScript = '' + repo = Repo("${config.name}", private=${boolPyLiteral config.repo.private}) + ''; }; }; - config = mkIf config.repo.enable { - setupScript = '' - repo = Repo("${config.name}", private=${boolPyLiteral config.repo.private}) - ''; - }; - }; in { options = { diff --git a/tests/nixos/fetch-git/testsupport/gitea.nix b/tests/nixos/fetch-git/testsupport/gitea.nix index cf87bb4662d..e63182639d3 100644 --- a/tests/nixos/fetch-git/testsupport/gitea.nix +++ b/tests/nixos/fetch-git/testsupport/gitea.nix @@ -1,4 +1,11 @@ -{ lib, nixpkgs, system, pkgs, ... }: let +{ + lib, + nixpkgs, + system, + pkgs, + ... +}: +let clientPrivateKey = pkgs.writeText "id_ed25519" '' -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW @@ -9,41 +16,62 @@ -----END OPENSSH PRIVATE KEY----- ''; - clientPublicKey = - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFt5a8eH8BYZYjoQhzXGVKKHJe1pw1D0p7O2Vb9VTLzB"; + clientPublicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFt5a8eH8BYZYjoQhzXGVKKHJe1pw1D0p7O2Vb9VTLzB"; -in { +in +{ imports = [ ../testsupport/setup.nix ../testsupport/gitea-repo.nix ]; nodes = { - gitea = { pkgs, ... }: { - services.gitea.enable = true; - services.gitea.settings.service.DISABLE_REGISTRATION = true; - services.gitea.settings.log.LEVEL = "Info"; - services.gitea.settings.database.LOG_SQL = false; - services.openssh.enable = true; - networking.firewall.allowedTCPPorts = [ 3000 ]; - environment.systemPackages = [ pkgs.git pkgs.gitea ]; + gitea = + { pkgs, ... }: + { + services.gitea.enable = true; + services.gitea.lfs.enable = true; + services.gitea.settings = { + service.DISABLE_REGISTRATION = true; + server = { + DOMAIN = "gitea"; + HTTP_PORT = 3000; + }; + log.LEVEL = "Info"; + database.LOG_SQL = false; + }; + services.openssh.enable = true; + networking.firewall.allowedTCPPorts = [ 3000 ]; + environment.systemPackages = [ + pkgs.git + pkgs.gitea + ]; - users.users.root.openssh.authorizedKeys.keys = [clientPublicKey]; + users.users.root.openssh.authorizedKeys.keys = [ clientPublicKey ]; - # TODO: remove this after updating to nixos-23.11 - nixpkgs.pkgs = lib.mkForce (import nixpkgs { - inherit system; - config.permittedInsecurePackages = [ - "gitea-1.19.4" + # TODO: remove this after updating to nixos-23.11 + nixpkgs.pkgs = lib.mkForce ( + import nixpkgs { + inherit system; + config.permittedInsecurePackages = [ + "gitea-1.19.4" + ]; + } + ); + }; + client = + { pkgs, ... }: + { + environment.systemPackages = [ + pkgs.git + pkgs.git-lfs ]; - }); - }; - client = { pkgs, ... }: { - environment.systemPackages = [ pkgs.git ]; - }; - }; - defaults = { pkgs, ... }: { - environment.systemPackages = [ pkgs.jq ]; + }; }; + defaults = + { pkgs, ... }: + { + environment.systemPackages = [ pkgs.jq ]; + }; setupScript = '' import shlex diff --git a/tests/nixos/fetch-git/testsupport/setup.nix b/tests/nixos/fetch-git/testsupport/setup.nix index a81d5614b44..c13386c7223 100644 --- a/tests/nixos/fetch-git/testsupport/setup.nix +++ b/tests/nixos/fetch-git/testsupport/setup.nix @@ -1,11 +1,16 @@ -{ lib, config, extendModules, ... }: +{ + lib, + config, + extendModules, + ... +}: let inherit (lib) mkOption types ; - indent = lib.replaceStrings ["\n"] ["\n "]; + indent = lib.replaceStrings [ "\n" ] [ "\n " ]; execTestCase = testCase: '' @@ -35,37 +40,39 @@ in description = '' The test cases. See `testScript`. ''; - type = types.listOf (types.submodule { - options.name = mkOption { - type = types.str; - description = '' - The name of the test case. + type = types.listOf ( + types.submodule { + options.name = mkOption { + type = types.str; + description = '' + The name of the test case. - A repository with that name will be set up on the gitea server and locally. - ''; - }; - options.description = mkOption { - type = types.str; - description = '' - A description of the test case. - ''; - }; - options.setupScript = mkOption { - type = types.lines; - description = '' - Python code that runs before the test case. - ''; - default = ""; - }; - options.script = mkOption { - type = types.lines; - description = '' - Python code that runs the test. + A repository with that name will be set up on the gitea server and locally. + ''; + }; + options.description = mkOption { + type = types.str; + description = '' + A description of the test case. + ''; + }; + options.setupScript = mkOption { + type = types.lines; + description = '' + Python code that runs before the test case. + ''; + default = ""; + }; + options.script = mkOption { + type = types.lines; + description = '' + Python code that runs the test. - Variables defined by the global `setupScript`, as well as `testCases.*.setupScript` will be available here. - ''; - }; - }); + Variables defined by the global `setupScript`, as well as `testCases.*.setupScript` will be available here. + ''; + }; + } + ); }; }; @@ -74,10 +81,12 @@ in environment.variables = { _NIX_FORCE_HTTP = "1"; }; - nix.settings.experimental-features = ["nix-command" "flakes"]; + nix.settings.experimental-features = [ + "nix-command" + "flakes" + ]; }; - setupScript = '' - ''; + setupScript = ''''; testScript = '' start_all(); diff --git a/tests/nixos/fetchurl.nix b/tests/nixos/fetchurl.nix index 243c0cacc6e..e8663debbcd 100644 --- a/tests/nixos/fetchurl.nix +++ b/tests/nixos/fetchurl.nix @@ -5,16 +5,20 @@ let - makeTlsCert = name: pkgs.runCommand name { - nativeBuildInputs = with pkgs; [ openssl ]; - } '' - mkdir -p $out - openssl req -x509 \ - -subj '/CN=${name}/' -days 49710 \ - -addext 'subjectAltName = DNS:${name}' \ - -keyout "$out/key.pem" -newkey ed25519 \ - -out "$out/cert.pem" -noenc - ''; + makeTlsCert = + name: + pkgs.runCommand name + { + nativeBuildInputs = with pkgs; [ openssl ]; + } + '' + mkdir -p $out + openssl req -x509 \ + -subj '/CN=${name}/' -days 49710 \ + -addext 'subjectAltName = DNS:${name}' \ + -keyout "$out/key.pem" -newkey ed25519 \ + -out "$out/cert.pem" -noenc + ''; goodCert = makeTlsCert "good"; badCert = makeTlsCert "bad"; @@ -22,42 +26,47 @@ let in { - name = "nss-preload"; + name = "fetchurl"; nodes = { - machine = { pkgs, ... }: { - services.nginx = { - enable = true; - - virtualHosts."good" = { - addSSL = true; - sslCertificate = "${goodCert}/cert.pem"; - sslCertificateKey = "${goodCert}/key.pem"; - root = pkgs.runCommand "nginx-root" {} '' - mkdir "$out" - echo 'hello world' > "$out/index.html" - ''; + machine = + { pkgs, ... }: + { + services.nginx = { + enable = true; + + virtualHosts."good" = { + addSSL = true; + sslCertificate = "${goodCert}/cert.pem"; + sslCertificateKey = "${goodCert}/key.pem"; + root = pkgs.runCommand "nginx-root" { } '' + mkdir "$out" + echo 'hello world' > "$out/index.html" + ''; + }; + + virtualHosts."bad" = { + addSSL = true; + sslCertificate = "${badCert}/cert.pem"; + sslCertificateKey = "${badCert}/key.pem"; + root = pkgs.runCommand "nginx-root" { } '' + mkdir "$out" + echo 'foobar' > "$out/index.html" + ''; + }; }; - virtualHosts."bad" = { - addSSL = true; - sslCertificate = "${badCert}/cert.pem"; - sslCertificateKey = "${badCert}/key.pem"; - root = pkgs.runCommand "nginx-root" {} '' - mkdir "$out" - echo 'foobar' > "$out/index.html" - ''; - }; - }; + security.pki.certificateFiles = [ "${goodCert}/cert.pem" ]; - security.pki.certificateFiles = [ "${goodCert}/cert.pem" ]; + networking.hosts."127.0.0.1" = [ + "good" + "bad" + ]; - networking.hosts."127.0.0.1" = [ "good" "bad" ]; + virtualisation.writableStore = true; - virtualisation.writableStore = true; - - nix.settings.experimental-features = "nix-command"; - }; + nix.settings.experimental-features = "nix-command"; + }; }; testScript = '' @@ -76,7 +85,7 @@ in # Fetching from a server with an untrusted cert should fail. err = machine.fail("nix build --no-substitute --expr 'import { url = \"https://bad/index.html\"; hash = \"sha256-rsBwZF/lPuOzdjBZN2E08FjMM3JHyXit0Xi2zN+wAZ8=\"; }' 2>&1") print(err) - assert "SSL certificate problem: self-signed certificate" in err + assert "SSL peer certificate or SSH remote key was not OK" in err # Fetching from a server with a trusted cert should work via environment variable override. machine.succeed("NIX_SSL_CERT_FILE=/tmp/cafile.pem nix build --no-substitute --expr 'import { url = \"https://bad/index.html\"; hash = \"sha256-rsBwZF/lPuOzdjBZN2E08FjMM3JHyXit0Xi2zN+wAZ8=\"; }'") diff --git a/tests/nixos/fsync.nix b/tests/nixos/fsync.nix index 99ac2b25d50..e215e5b3c25 100644 --- a/tests/nixos/fsync.nix +++ b/tests/nixos/fsync.nix @@ -1,4 +1,10 @@ -{ lib, config, nixpkgs, pkgs, ... }: +{ + lib, + config, + nixpkgs, + pkgs, + ... +}: let pkg1 = pkgs.go; @@ -8,32 +14,44 @@ in name = "fsync"; nodes.machine = - { config, lib, pkgs, ... }: - { virtualisation.emptyDiskImages = [ 1024 ]; + { + config, + lib, + pkgs, + ... + }: + { + virtualisation.emptyDiskImages = [ 1024 ]; environment.systemPackages = [ pkg1 ]; nix.settings.experimental-features = [ "nix-command" ]; nix.settings.fsync-store-paths = true; nix.settings.require-sigs = false; - boot.supportedFilesystems = [ "ext4" "btrfs" "xfs" ]; + boot.supportedFilesystems = [ + "ext4" + "btrfs" + "xfs" + ]; }; - testScript = { nodes }: '' - # fmt: off - for fs in ("ext4", "btrfs", "xfs"): - machine.succeed("mkfs.{} {} /dev/vdb".format(fs, "-F" if fs == "ext4" else "-f")) - machine.succeed("mkdir -p /mnt") - machine.succeed("mount /dev/vdb /mnt") - machine.succeed("sync") - machine.succeed("nix copy --offline ${pkg1} --to /mnt") - machine.crash() + testScript = + { nodes }: + '' + # fmt: off + for fs in ("ext4", "btrfs", "xfs"): + machine.succeed("mkfs.{} {} /dev/vdb".format(fs, "-F" if fs == "ext4" else "-f")) + machine.succeed("mkdir -p /mnt") + machine.succeed("mount /dev/vdb /mnt") + machine.succeed("sync") + machine.succeed("nix copy --offline ${pkg1} --to /mnt") + machine.crash() - machine.start() - machine.wait_for_unit("multi-user.target") - machine.succeed("mkdir -p /mnt") - machine.succeed("mount /dev/vdb /mnt") - machine.succeed("nix path-info --offline --store /mnt ${pkg1}") - machine.succeed("nix store verify --all --store /mnt --no-trust") + machine.start() + machine.wait_for_unit("multi-user.target") + machine.succeed("mkdir -p /mnt") + machine.succeed("mount /dev/vdb /mnt") + machine.succeed("nix path-info --offline --store /mnt ${pkg1}") + machine.succeed("nix store verify --all --store /mnt --no-trust") - machine.succeed("umount /dev/vdb") - ''; + machine.succeed("umount /dev/vdb") + ''; } diff --git a/tests/nixos/functional/as-trusted-user.nix b/tests/nixos/functional/as-trusted-user.nix index d6f825697e9..25c1b399c1c 100644 --- a/tests/nixos/functional/as-trusted-user.nix +++ b/tests/nixos/functional/as-trusted-user.nix @@ -4,7 +4,9 @@ imports = [ ./common.nix ]; nodes.machine = { - users.users.alice = { isNormalUser = true; }; + users.users.alice = { + isNormalUser = true; + }; nix.settings.trusted-users = [ "alice" ]; }; @@ -15,4 +17,4 @@ su --login --command "run-test-suite" alice >&2 """) ''; -} \ No newline at end of file +} diff --git a/tests/nixos/functional/as-user.nix b/tests/nixos/functional/as-user.nix index 1443f6e6ccd..b93c8d798a3 100644 --- a/tests/nixos/functional/as-user.nix +++ b/tests/nixos/functional/as-user.nix @@ -4,7 +4,9 @@ imports = [ ./common.nix ]; nodes.machine = { - users.users.alice = { isNormalUser = true; }; + users.users.alice = { + isNormalUser = true; + }; }; testScript = '' diff --git a/tests/nixos/functional/common.nix b/tests/nixos/functional/common.nix index 561271ba0ec..a2067c07dfb 100644 --- a/tests/nixos/functional/common.nix +++ b/tests/nixos/functional/common.nix @@ -2,9 +2,11 @@ let # FIXME (roberth) reference issue - inputDerivation = pkg: (pkg.overrideAttrs (o: { - disallowedReferences = [ ]; - })).inputDerivation; + inputDerivation = + pkg: + (pkg.overrideAttrs (o: { + disallowedReferences = [ ]; + })).inputDerivation; in { @@ -12,59 +14,63 @@ in # we skip it to save time. skipTypeCheck = true; - nodes.machine = { config, pkgs, ... }: { + nodes.machine = + { config, pkgs, ... }: + { - virtualisation.writableStore = true; - system.extraDependencies = [ - (inputDerivation config.nix.package) - ]; + virtualisation.writableStore = true; + system.extraDependencies = [ + (inputDerivation config.nix.package) + ]; - nix.settings.substituters = lib.mkForce []; + nix.settings.substituters = lib.mkForce [ ]; - environment.systemPackages = let - run-test-suite = pkgs.writeShellApplication { - name = "run-test-suite"; - runtimeInputs = [ - pkgs.meson - pkgs.ninja - pkgs.jq - pkgs.git + environment.systemPackages = + let + run-test-suite = pkgs.writeShellApplication { + name = "run-test-suite"; + runtimeInputs = [ + pkgs.meson + pkgs.ninja + pkgs.jq + pkgs.git - # Want to avoid `/run/current-system/sw/bin/bash` because we - # want a store path. Likewise for coreutils. - pkgs.bash - pkgs.coreutils - ]; - text = '' - set -x + # Want to avoid `/run/current-system/sw/bin/bash` because we + # want a store path. Likewise for coreutils. + pkgs.bash + pkgs.coreutils + ]; + text = '' + set -x - cat /proc/sys/fs/file-max - ulimit -Hn - ulimit -Sn + cat /proc/sys/fs/file-max + ulimit -Hn + ulimit -Sn - cd ~ + cd ~ - cp -r ${pkgs.nixComponents.nix-functional-tests.src} nix - chmod -R +w nix + cp -r ${pkgs.nixComponents2.nix-functional-tests.src} nix + chmod -R +w nix - chmod u+w nix/.version - echo ${pkgs.nixComponents.version} > nix/.version + chmod u+w nix/.version + echo ${pkgs.nixComponents2.version} > nix/.version - export isTestOnNixOS=1 + export isTestOnNixOS=1 - export NIX_REMOTE_=daemon - export NIX_REMOTE=daemon + export NIX_REMOTE_=daemon + export NIX_REMOTE=daemon - export NIX_STORE=${builtins.storeDir} + export NIX_STORE=${builtins.storeDir} - meson setup nix/tests/functional build - cd build - meson test -j1 --print-errorlogs - ''; - }; - in [ - run-test-suite - pkgs.git - ]; - }; + meson setup nix/tests/functional build + cd build + meson test -j1 --print-errorlogs + ''; + }; + in + [ + run-test-suite + pkgs.git + ]; + }; } diff --git a/tests/nixos/functional/symlinked-home.nix b/tests/nixos/functional/symlinked-home.nix new file mode 100644 index 00000000000..900543d0cfe --- /dev/null +++ b/tests/nixos/functional/symlinked-home.nix @@ -0,0 +1,38 @@ +/** + This test runs the functional tests on a NixOS system where the home directory + is symlinked to another location. + + The purpose of this test is to find cases where Nix uses low-level operations + that don't support symlinks on paths that include them. + + It is not a substitute for more intricate, use case-specific tests, but helps + catch common issues. +*/ +# TODO: add symlinked tmpdir +{ ... }: +{ + name = "functional-tests-on-nixos_user_symlinked-home"; + + imports = [ ./common.nix ]; + + nodes.machine = { + users.users.alice = { + isNormalUser = true; + }; + }; + + testScript = '' + machine.wait_for_unit("multi-user.target") + with subtest("prepare symlinked home"): + machine.succeed(""" + ( + set -x + mv /home/alice /home/alice.real + ln -s alice.real /home/alice + ) 1>&2 + """) + machine.succeed(""" + su --login --command "run-test-suite" alice >&2 + """) + ''; +} diff --git a/tests/nixos/git-submodules.nix b/tests/nixos/git-submodules.nix index 570b1822bf6..c6f53ada2dc 100644 --- a/tests/nixos/git-submodules.nix +++ b/tests/nixos/git-submodules.nix @@ -6,65 +6,74 @@ config = { name = lib.mkDefault "git-submodules"; - nodes = - { - remote = - { config, pkgs, ... }: - { - services.openssh.enable = true; - environment.systemPackages = [ pkgs.git ]; - }; + nodes = { + remote = + { config, pkgs, ... }: + { + services.openssh.enable = true; + environment.systemPackages = [ pkgs.git ]; + }; - client = - { config, lib, pkgs, ... }: - { - programs.ssh.extraConfig = "ConnectTimeout 30"; - environment.systemPackages = [ pkgs.git ]; - nix.extraOptions = "experimental-features = nix-command flakes"; - }; - }; + client = + { + config, + lib, + pkgs, + ... + }: + { + programs.ssh.extraConfig = "ConnectTimeout 30"; + environment.systemPackages = [ pkgs.git ]; + nix.extraOptions = "experimental-features = nix-command flakes"; + }; + }; - testScript = { nodes }: '' - # fmt: off - import subprocess + testScript = + { nodes }: + '' + # fmt: off + import subprocess - start_all() + start_all() - # Create an SSH key on the client. - subprocess.run([ - "${hostPkgs.openssh}/bin/ssh-keygen", "-t", "ed25519", "-f", "key", "-N", "" - ], capture_output=True, check=True) - client.succeed("mkdir -p -m 700 /root/.ssh") - client.copy_from_host("key", "/root/.ssh/id_ed25519") - client.succeed("chmod 600 /root/.ssh/id_ed25519") + # Create an SSH key on the client. + subprocess.run([ + "${hostPkgs.openssh}/bin/ssh-keygen", "-t", "ed25519", "-f", "key", "-N", "" + ], capture_output=True, check=True) + client.succeed("mkdir -p -m 700 /root/.ssh") + client.copy_from_host("key", "/root/.ssh/id_ed25519") + client.succeed("chmod 600 /root/.ssh/id_ed25519") - # Install the SSH key on the builders. - client.wait_for_unit("network.target") + # Install the SSH key on the builders. + client.wait_for_unit("network-addresses-eth1.service") - remote.succeed("mkdir -p -m 700 /root/.ssh") - remote.copy_from_host("key.pub", "/root/.ssh/authorized_keys") - remote.wait_for_unit("sshd") - client.succeed(f"ssh -o StrictHostKeyChecking=no {remote.name} 'echo hello world'") + remote.succeed("mkdir -p -m 700 /root/.ssh") + remote.copy_from_host("key.pub", "/root/.ssh/authorized_keys") + remote.wait_for_unit("sshd") + remote.wait_for_unit("multi-user.target") + remote.wait_for_unit("network-addresses-eth1.service") + client.wait_for_unit("network-addresses-eth1.service") + client.succeed(f"ssh -o StrictHostKeyChecking=no {remote.name} 'echo hello world'") - remote.succeed(""" - git init bar - git -C bar config user.email foobar@example.com - git -C bar config user.name Foobar - echo test >> bar/content - git -C bar add content - git -C bar commit -m 'Initial commit' - """) + remote.succeed(""" + git init bar + git -C bar config user.email foobar@example.com + git -C bar config user.name Foobar + echo test >> bar/content + git -C bar add content + git -C bar commit -m 'Initial commit' + """) - client.succeed(f""" - git init foo - git -C foo config user.email foobar@example.com - git -C foo config user.name Foobar - git -C foo submodule add root@{remote.name}:/tmp/bar sub - git -C foo add sub - git -C foo commit -m 'Add submodule' - """) + client.succeed(f""" + git init foo + git -C foo config user.email foobar@example.com + git -C foo config user.name Foobar + git -C foo submodule add root@{remote.name}:/tmp/bar sub + git -C foo add sub + git -C foo commit -m 'Add submodule' + """) - client.succeed("nix --flake-registry \"\" flake prefetch 'git+file:///tmp/foo?submodules=1&ref=master'") - ''; + client.succeed("nix --flake-registry \"\" flake prefetch 'git+file:///tmp/foo?submodules=1&ref=master'") + ''; }; } diff --git a/tests/nixos/github-flakes.nix b/tests/nixos/github-flakes.nix index 8e646f6dda2..91fd6b06234 100644 --- a/tests/nixos/github-flakes.nix +++ b/tests/nixos/github-flakes.nix @@ -1,21 +1,25 @@ -{ lib, config, nixpkgs, ... }: +{ + lib, + config, + nixpkgs, + ... +}: let pkgs = config.nodes.client.nixpkgs.pkgs; # Generate a fake root CA and a fake api.github.com / github.com / channels.nixos.org certificate. - cert = pkgs.runCommand "cert" { nativeBuildInputs = [ pkgs.openssl ]; } - '' - mkdir -p $out + cert = pkgs.runCommand "cert" { nativeBuildInputs = [ pkgs.openssl ]; } '' + mkdir -p $out - openssl genrsa -out ca.key 2048 - openssl req -new -x509 -days 36500 -key ca.key \ - -subj "/C=NL/ST=Denial/L=Springfield/O=Dis/CN=Root CA" -out $out/ca.crt + openssl genrsa -out ca.key 2048 + openssl req -new -x509 -days 36500 -key ca.key \ + -subj "/C=NL/ST=Denial/L=Springfield/O=Dis/CN=Root CA" -out $out/ca.crt - openssl req -newkey rsa:2048 -nodes -keyout $out/server.key \ - -subj "/C=CN/ST=Denial/L=Springfield/O=Dis/CN=github.com" -out server.csr - openssl x509 -req -extfile <(printf "subjectAltName=DNS:api.github.com,DNS:github.com,DNS:channels.nixos.org") \ - -days 36500 -in server.csr -CA $out/ca.crt -CAkey ca.key -CAcreateserial -out $out/server.crt - ''; + openssl req -newkey rsa:2048 -nodes -keyout $out/server.key \ + -subj "/C=CN/ST=Denial/L=Springfield/O=Dis/CN=github.com" -out server.csr + openssl x509 -req -extfile <(printf "subjectAltName=DNS:api.github.com,DNS:github.com,DNS:channels.nixos.org") \ + -days 36500 -in server.csr -CA $out/ca.crt -CAkey ca.key -CAcreateserial -out $out/server.crt + ''; registry = pkgs.writeTextFile { name = "registry"; @@ -53,166 +57,190 @@ let private-flake-rev = "9f1dd0df5b54a7dc75b618034482ed42ce34383d"; - private-flake-api = pkgs.runCommand "private-flake" {} - '' - mkdir -p $out/{commits,tarball} + private-flake-api = pkgs.runCommand "private-flake" { } '' + mkdir -p $out/{commits,tarball} - # Setup https://docs.github.com/en/rest/commits/commits#get-a-commit - echo '{"sha": "${private-flake-rev}", "commit": {"tree": {"sha": "ffffffffffffffffffffffffffffffffffffffff"}}}' > $out/commits/HEAD + # Setup https://docs.github.com/en/rest/commits/commits#get-a-commit + echo '{"sha": "${private-flake-rev}", "commit": {"tree": {"sha": "ffffffffffffffffffffffffffffffffffffffff"}}}' > $out/commits/HEAD - # Setup tarball download via API - dir=private-flake - mkdir $dir - echo '{ outputs = {...}: {}; }' > $dir/flake.nix - tar cfz $out/tarball/${private-flake-rev} $dir --hard-dereference - ''; + # Setup tarball download via API + dir=private-flake + mkdir $dir + echo '{ outputs = {...}: {}; }' > $dir/flake.nix + tar cfz $out/tarball/${private-flake-rev} $dir --hard-dereference + ''; - nixpkgs-api = pkgs.runCommand "nixpkgs-flake" {} - '' - mkdir -p $out/commits + nixpkgs-api = pkgs.runCommand "nixpkgs-flake" { } '' + mkdir -p $out/commits - # Setup https://docs.github.com/en/rest/commits/commits#get-a-commit - echo '{"sha": "${nixpkgs.rev}", "commit": {"tree": {"sha": "ffffffffffffffffffffffffffffffffffffffff"}}}' > $out/commits/HEAD - ''; + # Setup https://docs.github.com/en/rest/commits/commits#get-a-commit + echo '{"sha": "${nixpkgs.rev}", "commit": {"tree": {"sha": "ffffffffffffffffffffffffffffffffffffffff"}}}' > $out/commits/HEAD + ''; - archive = pkgs.runCommand "nixpkgs-flake" {} - '' - mkdir -p $out/archive + archive = pkgs.runCommand "nixpkgs-flake" { } '' + mkdir -p $out/archive - dir=NixOS-nixpkgs-${nixpkgs.shortRev} - cp -prd ${nixpkgs} $dir - # Set the correct timestamp in the tarball. - find $dir -print0 | xargs -0 touch -h -t ${builtins.substring 0 12 nixpkgs.lastModifiedDate}.${builtins.substring 12 2 nixpkgs.lastModifiedDate} -- - tar cfz $out/archive/${nixpkgs.rev}.tar.gz $dir --hard-dereference - ''; + dir=NixOS-nixpkgs-${nixpkgs.shortRev} + cp -rd --preserve=ownership,timestamps ${nixpkgs} $dir + # Set the correct timestamp in the tarball. + find $dir -print0 | xargs -0 touch -h -t ${builtins.substring 0 12 nixpkgs.lastModifiedDate}.${ + builtins.substring 12 2 nixpkgs.lastModifiedDate + } -- + tar cfz $out/archive/${nixpkgs.rev}.tar.gz $dir --hard-dereference + ''; in { name = "github-flakes"; - nodes = - { - github = - { config, pkgs, ... }: - { networking.firewall.allowedTCPPorts = [ 80 443 ]; - - services.httpd.enable = true; - services.httpd.adminAddr = "foo@example.org"; - services.httpd.extraConfig = '' - ErrorLog syslog:local6 - ''; - services.httpd.virtualHosts."channels.nixos.org" = - { forceSSL = true; - sslServerKey = "${cert}/server.key"; - sslServerCert = "${cert}/server.crt"; - servedDirs = - [ { urlPath = "/"; - dir = registry; - } - ]; - }; - services.httpd.virtualHosts."api.github.com" = - { forceSSL = true; - sslServerKey = "${cert}/server.key"; - sslServerCert = "${cert}/server.crt"; - servedDirs = - [ { urlPath = "/repos/NixOS/nixpkgs"; - dir = nixpkgs-api; - } - { urlPath = "/repos/fancy-enterprise/private-flake"; - dir = private-flake-api; - } - ]; - }; - services.httpd.virtualHosts."github.com" = - { forceSSL = true; - sslServerKey = "${cert}/server.key"; - sslServerCert = "${cert}/server.crt"; - servedDirs = - [ { urlPath = "/NixOS/nixpkgs"; - dir = archive; - } - ]; - }; + nodes = { + github = + { config, pkgs, ... }: + { + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; + + services.httpd.enable = true; + services.httpd.adminAddr = "foo@example.org"; + services.httpd.extraConfig = '' + ErrorLog syslog:local6 + ''; + services.httpd.virtualHosts."channels.nixos.org" = { + forceSSL = true; + sslServerKey = "${cert}/server.key"; + sslServerCert = "${cert}/server.crt"; + servedDirs = [ + { + urlPath = "/"; + dir = registry; + } + ]; }; - - client = - { config, lib, pkgs, nodes, ... }: - { virtualisation.writableStore = true; - virtualisation.diskSize = 2048; - virtualisation.additionalPaths = [ pkgs.hello pkgs.fuse ]; - virtualisation.memorySize = 4096; - nix.settings.substituters = lib.mkForce [ ]; - nix.extraOptions = "experimental-features = nix-command flakes"; - networking.hosts.${(builtins.head nodes.github.networking.interfaces.eth1.ipv4.addresses).address} = - [ "channels.nixos.org" "api.github.com" "github.com" ]; - security.pki.certificateFiles = [ "${cert}/ca.crt" ]; + services.httpd.virtualHosts."api.github.com" = { + forceSSL = true; + sslServerKey = "${cert}/server.key"; + sslServerCert = "${cert}/server.crt"; + servedDirs = [ + { + urlPath = "/repos/NixOS/nixpkgs"; + dir = nixpkgs-api; + } + { + urlPath = "/repos/fancy-enterprise/private-flake"; + dir = private-flake-api; + } + ]; }; - }; - - testScript = { nodes }: '' - # fmt: off - import json - import time - - start_all() - - def cat_log(): - github.succeed("cat /var/log/httpd/*.log >&2") - - github.wait_for_unit("httpd.service") - - client.succeed("curl -v https://github.com/ >&2") - out = client.succeed("nix registry list") - print(out) - assert "github:NixOS/nixpkgs" in out, "nixpkgs flake not found" - assert "github:fancy-enterprise/private-flake" in out, "private flake not found" - cat_log() - - # If no github access token is provided, nix should use the public archive url... - out = client.succeed("nix flake metadata nixpkgs --json") - print(out) - info = json.loads(out) - assert info["revision"] == "${nixpkgs.rev}", f"revision mismatch: {info['revision']} != ${nixpkgs.rev}" - cat_log() - - # ... otherwise it should use the API - out = client.succeed("nix flake metadata private-flake --json --access-tokens github.com=ghp_000000000000000000000000000000000000 --tarball-ttl 0") - print(out) - info = json.loads(out) - assert info["revision"] == "${private-flake-rev}", f"revision mismatch: {info['revision']} != ${private-flake-rev}" - assert info["fingerprint"] - cat_log() - - # Fetching with the resolved URL should produce the same result. - info2 = json.loads(client.succeed(f"nix flake metadata {info['url']} --json --access-tokens github.com=ghp_000000000000000000000000000000000000 --tarball-ttl 0")) - print(info["fingerprint"], info2["fingerprint"]) - assert info["fingerprint"] == info2["fingerprint"], "fingerprint mismatch" - - client.succeed("nix registry pin nixpkgs") - client.succeed("nix flake metadata nixpkgs --tarball-ttl 0 >&2") - - # Test fetchTree on a github URL. - hash = client.succeed(f"nix eval --no-trust-tarballs-from-git-forges --raw --expr '(fetchTree {info['url']}).narHash'") - assert hash == info['locked']['narHash'] - - # Fetching without a narHash should succeed if trust-github is set and fail otherwise. - client.succeed(f"nix eval --raw --expr 'builtins.fetchTree github:github:fancy-enterprise/private-flake/{info['revision']}'") - out = client.fail(f"nix eval --no-trust-tarballs-from-git-forges --raw --expr 'builtins.fetchTree github:github:fancy-enterprise/private-flake/{info['revision']}' 2>&1") - assert "will not fetch unlocked input" in out, "--no-trust-tarballs-from-git-forges did not fail with the expected error" - - # Shut down the web server. The flake should be cached on the client. - github.succeed("systemctl stop httpd.service") - - info = json.loads(client.succeed("nix flake metadata nixpkgs --json")) - date = time.strftime("%Y%m%d%H%M%S", time.gmtime(info['lastModified'])) - assert date == "${nixpkgs.lastModifiedDate}", "time mismatch" - - client.succeed("nix build nixpkgs#hello") - - # The build shouldn't fail even with --tarball-ttl 0 (the server - # being down should not be a fatal error). - client.succeed("nix build nixpkgs#fuse --tarball-ttl 0") - ''; + services.httpd.virtualHosts."github.com" = { + forceSSL = true; + sslServerKey = "${cert}/server.key"; + sslServerCert = "${cert}/server.crt"; + servedDirs = [ + { + urlPath = "/NixOS/nixpkgs"; + dir = archive; + } + ]; + }; + }; + + client = + { + config, + lib, + pkgs, + nodes, + ... + }: + { + virtualisation.writableStore = true; + virtualisation.diskSize = 2048; + virtualisation.additionalPaths = [ + pkgs.hello + pkgs.fuse + ]; + virtualisation.memorySize = 4096; + nix.settings.substituters = lib.mkForce [ ]; + nix.extraOptions = "experimental-features = nix-command flakes"; + networking.hosts.${(builtins.head nodes.github.networking.interfaces.eth1.ipv4.addresses).address} = + [ + "channels.nixos.org" + "api.github.com" + "github.com" + ]; + security.pki.certificateFiles = [ "${cert}/ca.crt" ]; + }; + }; + + testScript = + { nodes }: + '' + # fmt: off + import json + import time + + start_all() + + def cat_log(): + github.succeed("cat /var/log/httpd/*.log >&2") + + github.wait_for_unit("httpd.service") + github.wait_for_unit("network-addresses-eth1.service") + + client.wait_for_unit("network-addresses-eth1.service") + client.succeed("curl -v https://github.com/ >&2") + out = client.succeed("nix registry list") + print(out) + assert "github:NixOS/nixpkgs" in out, "nixpkgs flake not found" + assert "github:fancy-enterprise/private-flake" in out, "private flake not found" + cat_log() + + # If no github access token is provided, nix should use the public archive url... + out = client.succeed("nix flake metadata nixpkgs --json") + print(out) + info = json.loads(out) + assert info["revision"] == "${nixpkgs.rev}", f"revision mismatch: {info['revision']} != ${nixpkgs.rev}" + cat_log() + + # ... otherwise it should use the API + out = client.succeed("nix flake metadata private-flake --json --access-tokens github.com=ghp_000000000000000000000000000000000000 --tarball-ttl 0") + print(out) + info = json.loads(out) + assert info["revision"] == "${private-flake-rev}", f"revision mismatch: {info['revision']} != ${private-flake-rev}" + assert info["fingerprint"] + cat_log() + + # Fetching with the resolved URL should produce the same result. + info2 = json.loads(client.succeed(f"nix flake metadata {info['url']} --json --access-tokens github.com=ghp_000000000000000000000000000000000000 --tarball-ttl 0")) + print(info["fingerprint"], info2["fingerprint"]) + assert info["fingerprint"] == info2["fingerprint"], "fingerprint mismatch" + + client.succeed("nix registry pin nixpkgs") + client.succeed("nix flake metadata nixpkgs --tarball-ttl 0 >&2") + + # Test fetchTree on a github URL. + hash = client.succeed(f"nix eval --no-trust-tarballs-from-git-forges --raw --expr '(fetchTree {info['url']}).narHash'") + assert hash == info['locked']['narHash'] + + # Fetching without a narHash should succeed if trust-github is set and fail otherwise. + client.succeed(f"nix eval --raw --expr 'builtins.fetchTree github:github:fancy-enterprise/private-flake/{info['revision']}'") + out = client.fail(f"nix eval --no-trust-tarballs-from-git-forges --raw --expr 'builtins.fetchTree github:github:fancy-enterprise/private-flake/{info['revision']}' 2>&1") + assert "doesn't fetch unlocked input" in out, "--no-trust-tarballs-from-git-forges did not fail with the expected error" + + # Shut down the web server. The flake should be cached on the client. + github.succeed("systemctl stop httpd.service") + + info = json.loads(client.succeed("nix flake metadata nixpkgs --json")) + date = time.strftime("%Y%m%d%H%M%S", time.gmtime(info['lastModified'])) + assert date == "${nixpkgs.lastModifiedDate}", "time mismatch" + + client.succeed("nix build nixpkgs#hello") + + # The build shouldn't fail even with --tarball-ttl 0 (the server + # being down should not be a fatal error). + client.succeed("nix build nixpkgs#fuse --tarball-ttl 0") + ''; } diff --git a/tests/nixos/gzip-content-encoding.nix b/tests/nixos/gzip-content-encoding.nix index a5a0033fd19..22d196c6186 100644 --- a/tests/nixos/gzip-content-encoding.nix +++ b/tests/nixos/gzip-content-encoding.nix @@ -30,42 +30,45 @@ in { name = "gzip-content-encoding"; - nodes = - { machine = + nodes = { + machine = { config, pkgs, ... }: - { networking.firewall.allowedTCPPorts = [ 80 ]; + { + networking.firewall.allowedTCPPorts = [ 80 ]; services.nginx.enable = true; - services.nginx.virtualHosts."localhost" = - { root = "${ztdCompressedFile}/share/"; - # Make sure that nginx really tries to compress the - # file on the fly with no regard to size/mime. - # http://nginx.org/en/docs/http/ngx_http_gzip_module.html - extraConfig = '' - gzip on; - gzip_types *; - gzip_proxied any; - gzip_min_length 0; - ''; - }; + services.nginx.virtualHosts."localhost" = { + root = "${ztdCompressedFile}/share/"; + # Make sure that nginx really tries to compress the + # file on the fly with no regard to size/mime. + # http://nginx.org/en/docs/http/ngx_http_gzip_module.html + extraConfig = '' + gzip on; + gzip_types *; + gzip_proxied any; + gzip_min_length 0; + ''; + }; virtualisation.writableStore = true; virtualisation.additionalPaths = with pkgs; [ file ]; nix.settings.substituters = lib.mkForce [ ]; }; - }; + }; # Check that when nix-prefetch-url is used with a zst tarball it does not get decompressed. - testScript = { nodes }: '' - # fmt: off - start_all() + testScript = + { nodes }: + '' + # fmt: off + start_all() - machine.wait_for_unit("nginx.service") - machine.succeed(""" - # Make sure that the file is properly compressed as the test would be meaningless otherwise - curl --compressed -v http://localhost/archive |& tr -s ' ' |& grep --ignore-case 'content-encoding: gzip' - archive_path=$(nix-prefetch-url http://localhost/archive --print-path | tail -n1) - [[ $(${fileCmd} --brief --mime-type $archive_path) == "application/zstd" ]] - tar --zstd -xf $archive_path - """) - ''; + machine.wait_for_unit("nginx.service") + machine.succeed(""" + # Make sure that the file is properly compressed as the test would be meaningless otherwise + curl --compressed -v http://localhost/archive |& tr -s ' ' |& grep --ignore-case 'content-encoding: gzip' + archive_path=$(nix-prefetch-url http://localhost/archive --print-path | tail -n1) + [[ $(${fileCmd} --brief --mime-type $archive_path) == "application/zstd" ]] + tar --zstd -xf $archive_path + """) + ''; } diff --git a/tests/nixos/nix-copy-closure.nix b/tests/nixos/nix-copy-closure.nix index b9daa0a1f90..d24930de060 100644 --- a/tests/nixos/nix-copy-closure.nix +++ b/tests/nixos/nix-copy-closure.nix @@ -1,6 +1,11 @@ # Test ‘nix-copy-closure’. -{ lib, config, nixpkgs, ... }: +{ + lib, + config, + nixpkgs, + ... +}: let pkgs = config.nodes.client.nixpkgs.pkgs; @@ -10,71 +15,88 @@ let pkgC = pkgs.hello; pkgD = pkgs.tmux; -in { +in +{ name = "nix-copy-closure"; - nodes = - { client = - { config, lib, pkgs, ... }: - { virtualisation.writableStore = true; - virtualisation.additionalPaths = [ pkgA pkgD.drvPath ]; - nix.settings.substituters = lib.mkForce [ ]; - }; - - server = - { config, pkgs, ... }: - { services.openssh.enable = true; - virtualisation.writableStore = true; - virtualisation.additionalPaths = [ pkgB pkgC ]; - }; - }; - - testScript = { nodes }: '' - # fmt: off - import subprocess - - start_all() - - # Create an SSH key on the client. - subprocess.run([ - "${pkgs.openssh}/bin/ssh-keygen", "-t", "ed25519", "-f", "key", "-N", "" - ], capture_output=True, check=True) - - client.succeed("mkdir -m 700 /root/.ssh") - client.copy_from_host("key", "/root/.ssh/id_ed25519") - client.succeed("chmod 600 /root/.ssh/id_ed25519") - - # Install the SSH key on the server. - server.succeed("mkdir -m 700 /root/.ssh") - server.copy_from_host("key.pub", "/root/.ssh/authorized_keys") - server.wait_for_unit("sshd") - client.wait_for_unit("network.target") - client.succeed(f"ssh -o StrictHostKeyChecking=no {server.name} 'echo hello world'") - - # Copy the closure of package A from the client to the server. - server.fail("nix-store --check-validity ${pkgA}") - client.succeed("nix-copy-closure --to server --gzip ${pkgA} >&2") - server.succeed("nix-store --check-validity ${pkgA}") - - # Copy the closure of package B from the server to the client. - client.fail("nix-store --check-validity ${pkgB}") - client.succeed("nix-copy-closure --from server --gzip ${pkgB} >&2") - client.succeed("nix-store --check-validity ${pkgB}") - - # Copy the closure of package C via the SSH substituter. - client.fail("nix-store -r ${pkgC}") - - # Copy the derivation of package D's derivation from the client to the server. - server.fail("nix-store --check-validity ${pkgD.drvPath}") - client.succeed("nix-copy-closure --to server --gzip ${pkgD.drvPath} >&2") - server.succeed("nix-store --check-validity ${pkgD.drvPath}") - - # FIXME - # client.succeed( - # "nix-store --option use-ssh-substituter true" - # " --option ssh-substituter-hosts root\@server" - # " -r ${pkgC} >&2" - # ) - # client.succeed("nix-store --check-validity ${pkgC}") - ''; + nodes = { + client = + { + config, + lib, + pkgs, + ... + }: + { + virtualisation.writableStore = true; + virtualisation.additionalPaths = [ + pkgA + pkgD.drvPath + ]; + nix.settings.substituters = lib.mkForce [ ]; + }; + + server = + { config, pkgs, ... }: + { + services.openssh.enable = true; + virtualisation.writableStore = true; + virtualisation.additionalPaths = [ + pkgB + pkgC + ]; + }; + }; + + testScript = + { nodes }: + '' + # fmt: off + import subprocess + + start_all() + + # Create an SSH key on the client. + subprocess.run([ + "${pkgs.openssh}/bin/ssh-keygen", "-t", "ed25519", "-f", "key", "-N", "" + ], capture_output=True, check=True) + + client.copy_from_host("key", "/root/.ssh/id_ed25519") + client.succeed("chmod 600 /root/.ssh/id_ed25519") + + # Install the SSH key on the server. + server.copy_from_host("key.pub", "/root/.ssh/authorized_keys") + server.wait_for_unit("sshd") + server.wait_for_unit("multi-user.target") + server.wait_for_unit("network-addresses-eth1.service") + + client.wait_for_unit("network-addresses-eth1.service") + client.succeed(f"ssh -o StrictHostKeyChecking=no {server.name} 'echo hello world'") + + # Copy the closure of package A from the client to the server. + server.fail("nix-store --check-validity ${pkgA}") + client.succeed("nix-copy-closure --to server --gzip ${pkgA} >&2") + server.succeed("nix-store --check-validity ${pkgA}") + + # Copy the closure of package B from the server to the client. + client.fail("nix-store --check-validity ${pkgB}") + client.succeed("nix-copy-closure --from server --gzip ${pkgB} >&2") + client.succeed("nix-store --check-validity ${pkgB}") + + # Copy the closure of package C via the SSH substituter. + client.fail("nix-store -r ${pkgC}") + + # Copy the derivation of package D's derivation from the client to the server. + server.fail("nix-store --check-validity ${pkgD.drvPath}") + client.succeed("nix-copy-closure --to server --gzip ${pkgD.drvPath} >&2") + server.succeed("nix-store --check-validity ${pkgD.drvPath}") + + # FIXME + # client.succeed( + # "nix-store --option use-ssh-substituter true" + # " --option ssh-substituter-hosts root\@server" + # " -r ${pkgC} >&2" + # ) + # client.succeed("nix-store --check-validity ${pkgC}") + ''; } diff --git a/tests/nixos/nix-copy.nix b/tests/nixos/nix-copy.nix index 8691d01383f..64de622de76 100644 --- a/tests/nixos/nix-copy.nix +++ b/tests/nixos/nix-copy.nix @@ -2,7 +2,13 @@ # Run interactively with: # rm key key.pub; nix run .#hydraJobs.tests.nix-copy.driverInteractive -{ lib, config, nixpkgs, hostPkgs, ... }: +{ + lib, + config, + nixpkgs, + hostPkgs, + ... +}: let pkgs = config.nodes.client.nixpkgs.pkgs; @@ -12,98 +18,117 @@ let pkgC = pkgs.hello; pkgD = pkgs.tmux; -in { +in +{ name = "nix-copy"; enableOCR = true; - nodes = - { client = - { config, lib, pkgs, ... }: - { virtualisation.writableStore = true; - virtualisation.additionalPaths = [ pkgA pkgD.drvPath ]; - nix.settings.substituters = lib.mkForce [ ]; - nix.settings.experimental-features = [ "nix-command" ]; - services.getty.autologinUser = "root"; - programs.ssh.extraConfig = '' - Host * - ControlMaster auto - ControlPath ~/.ssh/master-%h:%r@%n:%p - ControlPersist 15m - ''; - }; - - server = - { config, pkgs, ... }: - { services.openssh.enable = true; - services.openssh.settings.PermitRootLogin = "yes"; - users.users.root.hashedPasswordFile = null; - users.users.root.password = "foobar"; - virtualisation.writableStore = true; - virtualisation.additionalPaths = [ pkgB pkgC ]; - }; - }; - - testScript = { nodes }: '' - # fmt: off - import subprocess - - # Create an SSH key on the client. - subprocess.run([ - "${pkgs.openssh}/bin/ssh-keygen", "-t", "ed25519", "-f", "key", "-N", "" - ], capture_output=True, check=True) - - start_all() - - server.wait_for_unit("sshd") - client.wait_for_unit("network.target") - client.wait_for_unit("getty@tty1.service") - # Either the prompt: ]# - # or an OCR misreading of it: 1# - client.wait_for_text("[]1]#") - - # Copy the closure of package A from the client to the server using password authentication, - # and check that all prompts are visible - server.fail("nix-store --check-validity ${pkgA}") - client.send_chars("nix copy --to ssh://server ${pkgA} >&2; echo -n do; echo ne\n") - client.wait_for_text("continue connecting") - client.send_chars("yes\n") - client.wait_for_text("Password:") - client.send_chars("foobar\n") - client.wait_for_text("done") - server.succeed("nix-store --check-validity ${pkgA}") - - # Check that ControlMaster is working - client.send_chars("nix copy --to ssh://server ${pkgA} >&2; echo done\n") - client.wait_for_text("done") - - client.copy_from_host("key", "/root/.ssh/id_ed25519") - client.succeed("chmod 600 /root/.ssh/id_ed25519") - - # Install the SSH key on the server. - server.copy_from_host("key.pub", "/root/.ssh/authorized_keys") - server.succeed("systemctl restart sshd") - client.succeed(f"ssh -o StrictHostKeyChecking=no {server.name} 'echo hello world'") - client.succeed(f"ssh -O check {server.name}") - client.succeed(f"ssh -O exit {server.name}") - client.fail(f"ssh -O check {server.name}") - - # Check that an explicit master will work - client.succeed(f"ssh -MNfS /tmp/master {server.name}") - client.succeed(f"ssh -S /tmp/master -O check {server.name}") - client.succeed("NIX_SSHOPTS='-oControlPath=/tmp/master' nix copy --to ssh://server ${pkgA} >&2") - client.succeed(f"ssh -S /tmp/master -O exit {server.name}") - - # Copy the closure of package B from the server to the client, using ssh-ng. - client.fail("nix-store --check-validity ${pkgB}") - # Shouldn't download untrusted paths by default - client.fail("nix copy --from ssh-ng://server ${pkgB} >&2") - client.succeed("nix copy --no-check-sigs --from ssh-ng://server ${pkgB} >&2") - client.succeed("nix-store --check-validity ${pkgB}") - - # Copy the derivation of package D's derivation from the client to the server. - server.fail("nix-store --check-validity ${pkgD.drvPath}") - client.succeed("nix copy --derivation --to ssh://server ${pkgD.drvPath} >&2") - server.succeed("nix-store --check-validity ${pkgD.drvPath}") - ''; + nodes = { + client = + { + config, + lib, + pkgs, + ... + }: + { + virtualisation.writableStore = true; + virtualisation.additionalPaths = [ + pkgA + pkgD.drvPath + ]; + nix.settings.substituters = lib.mkForce [ ]; + nix.settings.experimental-features = [ "nix-command" ]; + services.getty.autologinUser = "root"; + programs.ssh.extraConfig = '' + Host * + ControlMaster auto + ControlPath ~/.ssh/master-%h:%r@%n:%p + ControlPersist 15m + ''; + }; + + server = + { config, pkgs, ... }: + { + services.openssh.enable = true; + services.openssh.settings.PermitRootLogin = "yes"; + users.users.root.hashedPasswordFile = null; + users.users.root.password = "foobar"; + virtualisation.writableStore = true; + virtualisation.additionalPaths = [ + pkgB + pkgC + ]; + }; + }; + + testScript = + { nodes }: + '' + # fmt: off + import subprocess + + # Create an SSH key on the client. + subprocess.run([ + "${pkgs.openssh}/bin/ssh-keygen", "-t", "ed25519", "-f", "key", "-N", "" + ], capture_output=True, check=True) + + start_all() + + server.wait_for_unit("sshd") + server.wait_for_unit("multi-user.target") + server.wait_for_unit("network-addresses-eth1.service") + + client.wait_for_unit("network-addresses-eth1.service") + client.wait_for_unit("getty@tty1.service") + # Either the prompt: ]# + # or an OCR misreading of it: 1# + client.wait_for_text("[]1]#") + + # Copy the closure of package A from the client to the server using password authentication, + # and check that all prompts are visible + server.fail("nix-store --check-validity ${pkgA}") + client.send_chars("nix copy --to ssh://server ${pkgA} >&2; echo -n do; echo ne\n") + client.wait_for_text("continue connecting") + client.send_chars("yes\n") + client.wait_for_text("Password:") + client.send_chars("foobar\n") + client.wait_for_text("done") + server.succeed("nix-store --check-validity ${pkgA}") + + # Check that ControlMaster is working + client.send_chars("nix copy --to ssh://server ${pkgA} >&2; echo done\n") + client.wait_for_text("done") + + client.copy_from_host("key", "/root/.ssh/id_ed25519") + client.succeed("chmod 600 /root/.ssh/id_ed25519") + + # Install the SSH key on the server. + server.copy_from_host("key.pub", "/root/.ssh/authorized_keys") + server.succeed("systemctl restart sshd") + client.succeed(f"ssh -o StrictHostKeyChecking=no {server.name} 'echo hello world'") + client.succeed(f"ssh -O check {server.name}") + client.succeed(f"ssh -O exit {server.name}") + client.fail(f"ssh -O check {server.name}") + + # Check that an explicit master will work + client.succeed(f"ssh -MNfS /tmp/master {server.name}") + client.succeed(f"ssh -S /tmp/master -O check {server.name}") + client.succeed("NIX_SSHOPTS='-oControlPath=/tmp/master' nix copy --to ssh://server ${pkgA} >&2") + client.succeed(f"ssh -S /tmp/master -O exit {server.name}") + + # Copy the closure of package B from the server to the client, using ssh-ng. + client.fail("nix-store --check-validity ${pkgB}") + # Shouldn't download untrusted paths by default + client.fail("nix copy --from ssh-ng://server ${pkgB} >&2") + client.succeed("nix copy --no-check-sigs --from ssh-ng://server ${pkgB} >&2") + client.succeed("nix-store --check-validity ${pkgB}") + + # Copy the derivation of package D's derivation from the client to the server. + server.fail("nix-store --check-validity ${pkgD.drvPath}") + client.succeed("nix copy --derivation --to ssh://server ${pkgD.drvPath} >&2") + server.succeed("nix-store --check-validity ${pkgD.drvPath}") + ''; } diff --git a/tests/nixos/nix-docker.nix b/tests/nixos/nix-docker.nix index dfd50898894..f1c218585a6 100644 --- a/tests/nixos/nix-docker.nix +++ b/tests/nixos/nix-docker.nix @@ -1,15 +1,15 @@ # Test the container built by ../../docker.nix. -{ lib, config, nixpkgs, hostPkgs, ... }: +{ + config, + ... +}: let pkgs = config.nodes.machine.nixpkgs.pkgs; - nixImage = import ../../docker.nix { - inherit (config.nodes.machine.nixpkgs) pkgs; - }; - nixUserImage = import ../../docker.nix { - inherit (config.nodes.machine.nixpkgs) pkgs; + nixImage = pkgs.callPackage ../../docker.nix { }; + nixUserImage = pkgs.callPackage ../../docker.nix { name = "nix-user"; uid = 1000; gid = 1000; @@ -19,35 +19,54 @@ let containerTestScript = ./nix-docker-test.sh; -in { +in +{ name = "nix-docker"; - nodes = - { machine = - { config, lib, pkgs, ... }: - { virtualisation.diskSize = 4096; - }; - cache = - { config, lib, pkgs, ... }: - { virtualisation.additionalPaths = [ pkgs.stdenv pkgs.hello ]; - services.harmonia.enable = true; - networking.firewall.allowedTCPPorts = [ 5000 ]; - }; - }; - - testScript = { nodes }: '' - cache.wait_for_unit("harmonia.service") - - machine.succeed("mkdir -p /etc/containers") - machine.succeed("""echo '{"default":[{"type":"insecureAcceptAnything"}]}' > /etc/containers/policy.json""") - - machine.succeed("${pkgs.podman}/bin/podman load -i ${nixImage}") - machine.succeed("${pkgs.podman}/bin/podman run --rm nix nix --version") - machine.succeed("${pkgs.podman}/bin/podman run --rm -i nix < ${containerTestScript}") - - machine.succeed("${pkgs.podman}/bin/podman load -i ${nixUserImage}") - machine.succeed("${pkgs.podman}/bin/podman run --rm nix-user nix --version") - machine.succeed("${pkgs.podman}/bin/podman run --rm -i nix-user < ${containerTestScript}") - machine.succeed("[[ $(${pkgs.podman}/bin/podman run --rm nix-user stat -c %u /nix/store) = 1000 ]]") - ''; + nodes = { + machine = + { + config, + lib, + pkgs, + ... + }: + { + virtualisation.diskSize = 4096; + }; + cache = + { + config, + lib, + pkgs, + ... + }: + { + virtualisation.additionalPaths = [ + pkgs.stdenv + pkgs.hello + ]; + services.harmonia.enable = true; + networking.firewall.allowedTCPPorts = [ 5000 ]; + }; + }; + + testScript = + { nodes }: + '' + cache.wait_for_unit("harmonia.service") + cache.wait_for_unit("network-addresses-eth1.service") + + machine.succeed("mkdir -p /etc/containers") + machine.succeed("""echo '{"default":[{"type":"insecureAcceptAnything"}]}' > /etc/containers/policy.json""") + + machine.succeed("${pkgs.podman}/bin/podman load -i ${nixImage}") + machine.succeed("${pkgs.podman}/bin/podman run --rm nix nix --version") + machine.succeed("${pkgs.podman}/bin/podman run --rm -i nix < ${containerTestScript}") + + machine.succeed("${pkgs.podman}/bin/podman load -i ${nixUserImage}") + machine.succeed("${pkgs.podman}/bin/podman run --rm nix-user nix --version") + machine.succeed("${pkgs.podman}/bin/podman run --rm -i nix-user < ${containerTestScript}") + machine.succeed("[[ $(${pkgs.podman}/bin/podman run --rm nix-user stat -c %u /nix/store) = 1000 ]]") + ''; } diff --git a/tests/nixos/nss-preload.nix b/tests/nixos/nss-preload.nix index 610769c8df5..d99f22208cb 100644 --- a/tests/nixos/nss-preload.nix +++ b/tests/nixos/nss-preload.nix @@ -1,4 +1,9 @@ -{ lib, config, nixpkgs, ... }: +{ + lib, + config, + nixpkgs, + ... +}: let @@ -44,79 +49,119 @@ in name = "nss-preload"; nodes = { - http_dns = { lib, pkgs, config, ... }: { - networking.firewall.enable = false; - networking.interfaces.eth1.ipv6.addresses = lib.mkForce [ - { address = "fd21::1"; prefixLength = 64; } - ]; - networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ - { address = "192.168.0.1"; prefixLength = 24; } - ]; - - services.unbound = { - enable = true; - enableRootTrustAnchor = false; - settings = { - server = { - interface = [ "192.168.0.1" "fd21::1" "::1" "127.0.0.1" ]; - access-control = [ "192.168.0.0/24 allow" "fd21::/64 allow" "::1 allow" "127.0.0.0/8 allow" ]; - local-data = [ - ''"example.com. IN A 192.168.0.1"'' - ''"example.com. IN AAAA fd21::1"'' - ''"tarballs.nixos.org. IN A 192.168.0.1"'' - ''"tarballs.nixos.org. IN AAAA fd21::1"'' - ]; + http_dns = + { + lib, + pkgs, + config, + ... + }: + { + networking.firewall.enable = false; + networking.interfaces.eth1.ipv6.addresses = lib.mkForce [ + { + address = "fd21::1"; + prefixLength = 64; + } + ]; + networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ + { + address = "192.168.0.1"; + prefixLength = 24; + } + ]; + + services.unbound = { + enable = true; + enableRootTrustAnchor = false; + settings = { + server = { + interface = [ + "192.168.0.1" + "fd21::1" + "::1" + "127.0.0.1" + ]; + access-control = [ + "192.168.0.0/24 allow" + "fd21::/64 allow" + "::1 allow" + "127.0.0.0/8 allow" + ]; + local-data = [ + ''"example.com. IN A 192.168.0.1"'' + ''"example.com. IN AAAA fd21::1"'' + ''"tarballs.nixos.org. IN A 192.168.0.1"'' + ''"tarballs.nixos.org. IN AAAA fd21::1"'' + ]; + }; }; }; - }; - services.nginx = { - enable = true; - virtualHosts."example.com" = { - root = pkgs.runCommand "testdir" {} '' - mkdir "$out" - echo hello world > "$out/index.html" - ''; + services.nginx = { + enable = true; + virtualHosts."example.com" = { + root = pkgs.runCommand "testdir" { } '' + mkdir "$out" + echo hello world > "$out/index.html" + ''; + }; }; }; - }; # client consumes a remote resolver - client = { lib, nodes, pkgs, ... }: { - networking.useDHCP = false; - networking.nameservers = [ - (lib.head nodes.http_dns.networking.interfaces.eth1.ipv6.addresses).address - (lib.head nodes.http_dns.networking.interfaces.eth1.ipv4.addresses).address - ]; - networking.interfaces.eth1.ipv6.addresses = [ - { address = "fd21::10"; prefixLength = 64; } - ]; - networking.interfaces.eth1.ipv4.addresses = [ - { address = "192.168.0.10"; prefixLength = 24; } - ]; - - nix.settings.extra-sandbox-paths = lib.mkForce []; - nix.settings.substituters = lib.mkForce []; - nix.settings.sandbox = lib.mkForce true; - }; - }; - - testScript = { nodes, ... }: '' - http_dns.wait_for_unit("nginx") - http_dns.wait_for_open_port(80) - http_dns.wait_for_unit("unbound") - http_dns.wait_for_open_port(53) - - client.start() - client.wait_for_unit('multi-user.target') + client = + { + lib, + nodes, + pkgs, + ... + }: + { + networking.useDHCP = false; + networking.nameservers = [ + (lib.head nodes.http_dns.networking.interfaces.eth1.ipv6.addresses).address + (lib.head nodes.http_dns.networking.interfaces.eth1.ipv4.addresses).address + ]; + networking.interfaces.eth1.ipv6.addresses = [ + { + address = "fd21::10"; + prefixLength = 64; + } + ]; + networking.interfaces.eth1.ipv4.addresses = [ + { + address = "192.168.0.10"; + prefixLength = 24; + } + ]; - with subtest("can fetch data from a remote server outside sandbox"): - client.succeed("nix --version >&2") - client.succeed("curl -vvv http://example.com/index.html >&2") + nix.settings.extra-sandbox-paths = lib.mkForce [ ]; + nix.settings.substituters = lib.mkForce [ ]; + nix.settings.sandbox = lib.mkForce true; + }; + }; - with subtest("nix-build can lookup dns and fetch data"): - client.succeed(""" - nix-build ${nix-fetch} >&2 - """) - ''; + testScript = + { nodes, ... }: + '' + http_dns.wait_for_unit("network-addresses-eth1.service") + http_dns.wait_for_unit("nginx") + http_dns.wait_for_open_port(80) + http_dns.wait_for_unit("unbound") + http_dns.wait_for_open_port(53) + + client.start() + client.wait_for_unit('multi-user.target') + client.wait_for_unit('network-addresses-eth1.service') + + with subtest("can fetch data from a remote server outside sandbox"): + client.succeed("nix --version >&2") + client.succeed("curl -vvv http://example.com/index.html >&2") + + with subtest("nix-build can lookup dns and fetch data"): + client.succeed(""" + nix-build ${nix-fetch} >&2 + """) + ''; } diff --git a/tests/nixos/remote-builds-ssh-ng.nix b/tests/nixos/remote-builds-ssh-ng.nix index 926ec00fead..c298ab92d46 100644 --- a/tests/nixos/remote-builds-ssh-ng.nix +++ b/tests/nixos/remote-builds-ssh-ng.nix @@ -1,11 +1,17 @@ -test@{ config, lib, hostPkgs, ... }: +test@{ + config, + lib, + hostPkgs, + ... +}: let pkgs = config.nodes.client.nixpkgs.pkgs; # Trivial Nix expression to build remotely. - expr = config: nr: pkgs.writeText "expr.nix" - '' + expr = + config: nr: + pkgs.writeText "expr.nix" '' let utils = builtins.storePath ${config.system.build.extraUtils}; in derivation { name = "hello-${toString nr}"; @@ -41,84 +47,94 @@ in config = { name = lib.mkDefault "remote-builds-ssh-ng"; - nodes = - { - builder = - { config, pkgs, ... }: - { - imports = [ test.config.builders.config ]; - services.openssh.enable = true; - virtualisation.writableStore = true; - nix.settings.sandbox = true; - nix.settings.substituters = lib.mkForce [ ]; - }; - - client = - { config, lib, pkgs, ... }: - { - nix.settings.max-jobs = 0; # force remote building - nix.distributedBuilds = true; - nix.buildMachines = - [{ - hostName = "builder"; - sshUser = "root"; - sshKey = "/root/.ssh/id_ed25519"; - system = "i686-linux"; - maxJobs = 1; - protocol = "ssh-ng"; - }]; - virtualisation.writableStore = true; - virtualisation.additionalPaths = [ config.system.build.extraUtils ]; - nix.settings.substituters = lib.mkForce [ ]; - programs.ssh.extraConfig = "ConnectTimeout 30"; - }; - }; - - testScript = { nodes }: '' - # fmt: off - import subprocess - - start_all() - - # Create an SSH key on the client. - subprocess.run([ - "${hostPkgs.openssh}/bin/ssh-keygen", "-t", "ed25519", "-f", "key", "-N", "" - ], capture_output=True, check=True) - client.succeed("mkdir -p -m 700 /root/.ssh") - client.copy_from_host("key", "/root/.ssh/id_ed25519") - client.succeed("chmod 600 /root/.ssh/id_ed25519") - - # Install the SSH key on the builder. - client.wait_for_unit("network.target") - builder.succeed("mkdir -p -m 700 /root/.ssh") - builder.copy_from_host("key.pub", "/root/.ssh/authorized_keys") - builder.wait_for_unit("sshd") - client.succeed(f"ssh -o StrictHostKeyChecking=no {builder.name} 'echo hello world'") - - # Perform a build - out = client.succeed("nix-build ${expr nodes.client 1} 2> build-output") - - # Verify that the build was done on the builder - builder.succeed(f"test -e {out.strip()}") - - # Print the build log, prefix the log lines to avoid nix intercepting lines starting with @nix - buildOutput = client.succeed("sed -e 's/^/build-output:/' build-output") - print(buildOutput) - - # Make sure that we get the expected build output - client.succeed("grep -qF Hello build-output") - - # We don't want phase reporting in the build output - client.fail("grep -qF '@nix' build-output") - - # Get the log file - client.succeed(f"nix-store --read-log {out.strip()} > log-output") - # Prefix the log lines to avoid nix intercepting lines starting with @nix - logOutput = client.succeed("sed -e 's/^/log-file:/' log-output") - print(logOutput) - - # Check that we get phase reporting in the log file - client.succeed("grep -q '@nix {\"action\":\"setPhase\",\"phase\":\"buildPhase\"}' log-output") - ''; + nodes = { + builder = + { config, pkgs, ... }: + { + imports = [ test.config.builders.config ]; + services.openssh.enable = true; + virtualisation.writableStore = true; + nix.settings.sandbox = true; + nix.settings.substituters = lib.mkForce [ ]; + }; + + client = + { + config, + lib, + pkgs, + ... + }: + { + nix.settings.max-jobs = 0; # force remote building + nix.distributedBuilds = true; + nix.buildMachines = [ + { + hostName = "builder"; + sshUser = "root"; + sshKey = "/root/.ssh/id_ed25519"; + system = "i686-linux"; + maxJobs = 1; + protocol = "ssh-ng"; + } + ]; + virtualisation.writableStore = true; + virtualisation.additionalPaths = [ config.system.build.extraUtils ]; + nix.settings.substituters = lib.mkForce [ ]; + programs.ssh.extraConfig = "ConnectTimeout 30"; + }; + }; + + testScript = + { nodes }: + '' + # fmt: off + import subprocess + + start_all() + + # Create an SSH key on the client. + subprocess.run([ + "${hostPkgs.openssh}/bin/ssh-keygen", "-t", "ed25519", "-f", "key", "-N", "" + ], capture_output=True, check=True) + client.succeed("mkdir -p -m 700 /root/.ssh") + client.copy_from_host("key", "/root/.ssh/id_ed25519") + client.succeed("chmod 600 /root/.ssh/id_ed25519") + + # Install the SSH key on the builder. + client.wait_for_unit("network-addresses-eth1.service") + builder.succeed("mkdir -p -m 700 /root/.ssh") + builder.copy_from_host("key.pub", "/root/.ssh/authorized_keys") + builder.wait_for_unit("sshd") + builder.wait_for_unit("multi-user.target") + builder.wait_for_unit("network-addresses-eth1.service") + + client.succeed(f"ssh -o StrictHostKeyChecking=no {builder.name} 'echo hello world'") + + # Perform a build + out = client.succeed("nix-build ${expr nodes.client 1} 2> build-output") + + # Verify that the build was done on the builder + builder.succeed(f"test -e {out.strip()}") + + # Print the build log, prefix the log lines to avoid nix intercepting lines starting with @nix + buildOutput = client.succeed("sed -e 's/^/build-output:/' build-output") + print(buildOutput) + + # Make sure that we get the expected build output + client.succeed("grep -qF Hello build-output") + + # We don't want phase reporting in the build output + client.fail("grep -qF '@nix' build-output") + + # Get the log file + client.succeed(f"nix-store --read-log {out.strip()} > log-output") + # Prefix the log lines to avoid nix intercepting lines starting with @nix + logOutput = client.succeed("sed -e 's/^/log-file:/' log-output") + print(logOutput) + + # Check that we get phase reporting in the log file + client.succeed("grep -q '@nix {\"action\":\"setPhase\",\"phase\":\"buildPhase\"}' log-output") + ''; }; } diff --git a/tests/nixos/remote-builds.nix b/tests/nixos/remote-builds.nix index 84e5176b720..fbfff9a7dc7 100644 --- a/tests/nixos/remote-builds.nix +++ b/tests/nixos/remote-builds.nix @@ -1,6 +1,11 @@ # Test Nix's remote build feature. -test@{ config, lib, hostPkgs, ... }: +test@{ + config, + lib, + hostPkgs, + ... +}: let pkgs = config.nodes.client.nixpkgs.pkgs; @@ -21,8 +26,9 @@ let }; # Trivial Nix expression to build remotely. - expr = config: nr: pkgs.writeText "expr.nix" - '' + expr = + config: nr: + pkgs.writeText "expr.nix" '' let utils = builtins.storePath ${config.system.build.extraUtils}; in derivation { name = "hello-${toString nr}"; @@ -52,106 +58,112 @@ in config = { name = lib.mkDefault "remote-builds"; - nodes = - { - builder1 = builder; - builder2 = builder; - - client = - { config, lib, pkgs, ... }: - { - nix.settings.max-jobs = 0; # force remote building - nix.distributedBuilds = true; - nix.buildMachines = - [ - { - hostName = "builder1"; - sshUser = "root"; - sshKey = "/root/.ssh/id_ed25519"; - system = "i686-linux"; - maxJobs = 1; - } - { - hostName = "builder2"; - sshUser = "root"; - sshKey = "/root/.ssh/id_ed25519"; - system = "i686-linux"; - maxJobs = 1; - } - ]; - virtualisation.writableStore = true; - virtualisation.additionalPaths = [ config.system.build.extraUtils ]; - nix.settings.substituters = lib.mkForce [ ]; - programs.ssh.extraConfig = "ConnectTimeout 30"; - environment.systemPackages = [ - # `bad-shell` is used to make sure Nix works in an environment with a misbehaving shell. - # - # More realistically, a bad shell would still run the command ("echo started") - # but considering that our solution is to avoid this shell (set via $SHELL), we - # don't need to bother with a more functional mock shell. - (pkgs.writeScriptBin "bad-shell" '' - #!${pkgs.runtimeShell} - echo "Hello, I am a broken shell" - '') - ]; - }; - }; - - testScript = { nodes }: '' - # fmt: off - import subprocess - - start_all() - - # Create an SSH key on the client. - subprocess.run([ - "${hostPkgs.openssh}/bin/ssh-keygen", "-t", "ed25519", "-f", "key", "-N", "" - ], capture_output=True, check=True) - client.succeed("mkdir -p -m 700 /root/.ssh") - client.copy_from_host("key", "/root/.ssh/id_ed25519") - client.succeed("chmod 600 /root/.ssh/id_ed25519") - - # Install the SSH key on the builders. - client.wait_for_unit("network.target") - for builder in [builder1, builder2]: - builder.succeed("mkdir -p -m 700 /root/.ssh") - builder.copy_from_host("key.pub", "/root/.ssh/authorized_keys") - builder.wait_for_unit("sshd") - # Make sure the builder can handle our login correctly - builder.wait_for_unit("multi-user.target") - # Make sure there's no funny business on the client either - # (should not be necessary, but we have reason to be careful) - client.wait_for_unit("multi-user.target") - client.succeed(f""" - ssh -o StrictHostKeyChecking=no {builder.name} \ - 'echo hello world on $(hostname)' >&2 - """) - - ${lib.optionalString supportsBadShell '' - # Check that SSH uses SHELL for LocalCommand, as expected, and check that - # our test setup here is working. The next test will use this bad SHELL. - client.succeed(f"SHELL=$(which bad-shell) ssh -oLocalCommand='true' -oPermitLocalCommand=yes {builder1.name} 'echo hello world' | grep -F 'Hello, I am a broken shell'") - ''} - - # Perform a build and check that it was performed on the builder. - out = client.succeed( - "${lib.optionalString supportsBadShell "SHELL=$(which bad-shell)"} nix-build ${expr nodes.client 1} 2> build-output", - "grep -q Hello build-output" - ) - builder1.succeed(f"test -e {out}") - - # And a parallel build. - paths = client.succeed(r'nix-store -r $(nix-instantiate ${expr nodes.client 2})\!out $(nix-instantiate ${expr nodes.client 3})\!out') - out1, out2 = paths.split() - builder1.succeed(f"test -e {out1} -o -e {out2}") - builder2.succeed(f"test -e {out1} -o -e {out2}") - - # And a failing build. - client.fail("nix-build ${expr nodes.client 5}") - - # Test whether the build hook automatically skips unavailable builders. - builder1.block() - client.succeed("nix-build ${expr nodes.client 4}") - ''; + nodes = { + builder1 = builder; + builder2 = builder; + + client = + { + config, + lib, + pkgs, + ... + }: + { + nix.settings.max-jobs = 0; # force remote building + nix.distributedBuilds = true; + nix.buildMachines = [ + { + hostName = "builder1"; + sshUser = "root"; + sshKey = "/root/.ssh/id_ed25519"; + system = "i686-linux"; + maxJobs = 1; + } + { + hostName = "builder2"; + sshUser = "root"; + sshKey = "/root/.ssh/id_ed25519"; + system = "i686-linux"; + maxJobs = 1; + } + ]; + virtualisation.writableStore = true; + virtualisation.additionalPaths = [ config.system.build.extraUtils ]; + nix.settings.substituters = lib.mkForce [ ]; + programs.ssh.extraConfig = "ConnectTimeout 30"; + environment.systemPackages = [ + # `bad-shell` is used to make sure Nix works in an environment with a misbehaving shell. + # + # More realistically, a bad shell would still run the command ("echo started") + # but considering that our solution is to avoid this shell (set via $SHELL), we + # don't need to bother with a more functional mock shell. + (pkgs.writeScriptBin "bad-shell" '' + #!${pkgs.runtimeShell} + echo "Hello, I am a broken shell" + '') + ]; + }; + }; + + testScript = + { nodes }: + '' + # fmt: off + import subprocess + + start_all() + + # Create an SSH key on the client. + subprocess.run([ + "${hostPkgs.openssh}/bin/ssh-keygen", "-t", "ed25519", "-f", "key", "-N", "" + ], capture_output=True, check=True) + client.succeed("mkdir -p -m 700 /root/.ssh") + client.copy_from_host("key", "/root/.ssh/id_ed25519") + client.succeed("chmod 600 /root/.ssh/id_ed25519") + + # Install the SSH key on the builders. + client.wait_for_unit("network-addresses-eth1.service") + for builder in [builder1, builder2]: + builder.succeed("mkdir -p -m 700 /root/.ssh") + builder.copy_from_host("key.pub", "/root/.ssh/authorized_keys") + builder.wait_for_unit("sshd") + builder.wait_for_unit("network-addresses-eth1.service") + # Make sure the builder can handle our login correctly + builder.wait_for_unit("multi-user.target") + # Make sure there's no funny business on the client either + # (should not be necessary, but we have reason to be careful) + client.wait_for_unit("multi-user.target") + client.succeed(f""" + ssh -o StrictHostKeyChecking=no {builder.name} \ + 'echo hello world on $(hostname)' >&2 + """) + + ${lib.optionalString supportsBadShell '' + # Check that SSH uses SHELL for LocalCommand, as expected, and check that + # our test setup here is working. The next test will use this bad SHELL. + client.succeed(f"SHELL=$(which bad-shell) ssh -oLocalCommand='true' -oPermitLocalCommand=yes {builder1.name} 'echo hello world' | grep -F 'Hello, I am a broken shell'") + ''} + + # Perform a build and check that it was performed on the builder. + out = client.succeed( + "${lib.optionalString supportsBadShell "SHELL=$(which bad-shell)"} nix-build ${expr nodes.client 1} 2> build-output", + "grep -q Hello build-output" + ) + builder1.succeed(f"test -e {out}") + + # And a parallel build. + paths = client.succeed(r'nix-store -r $(nix-instantiate ${expr nodes.client 2})\!out $(nix-instantiate ${expr nodes.client 3})\!out') + out1, out2 = paths.split() + builder1.succeed(f"test -e {out1} -o -e {out2}") + builder2.succeed(f"test -e {out1} -o -e {out2}") + + # And a failing build. + client.fail("nix-build ${expr nodes.client 5}") + + # Test whether the build hook automatically skips unavailable builders. + builder1.block() + client.succeed("nix-build ${expr nodes.client 4}") + ''; }; } diff --git a/tests/nixos/s3-binary-cache-store.nix b/tests/nixos/s3-binary-cache-store.nix index 6c51fcba5b1..a22e4c2c28f 100644 --- a/tests/nixos/s3-binary-cache-store.nix +++ b/tests/nixos/s3-binary-cache-store.nix @@ -1,4 +1,9 @@ -{ lib, config, nixpkgs, ... }: +{ + lib, + config, + nixpkgs, + ... +}: let pkgs = config.nodes.client.nixpkgs.pkgs; @@ -10,57 +15,84 @@ let env = "AWS_ACCESS_KEY_ID=${accessKey} AWS_SECRET_ACCESS_KEY=${secretKey}"; storeUrl = "s3://my-cache?endpoint=http://server:9000®ion=eu-west-1"; + objectThatDoesNotExist = "s3://my-cache/foo-that-does-not-exist?endpoint=http://server:9000®ion=eu-west-1"; -in { +in +{ name = "s3-binary-cache-store"; - nodes = - { server = - { config, lib, pkgs, ... }: - { virtualisation.writableStore = true; - virtualisation.additionalPaths = [ pkgA ]; - environment.systemPackages = [ pkgs.minio-client ]; - nix.extraOptions = "experimental-features = nix-command"; - services.minio = { - enable = true; - region = "eu-west-1"; - rootCredentialsFile = pkgs.writeText "minio-credentials-full" '' - MINIO_ROOT_USER=${accessKey} - MINIO_ROOT_PASSWORD=${secretKey} - ''; - }; - networking.firewall.allowedTCPPorts = [ 9000 ]; + nodes = { + server = + { + config, + lib, + pkgs, + ... + }: + { + virtualisation.writableStore = true; + virtualisation.additionalPaths = [ pkgA ]; + environment.systemPackages = [ pkgs.minio-client ]; + nix.extraOptions = '' + experimental-features = nix-command + substituters = + ''; + services.minio = { + enable = true; + region = "eu-west-1"; + rootCredentialsFile = pkgs.writeText "minio-credentials-full" '' + MINIO_ROOT_USER=${accessKey} + MINIO_ROOT_PASSWORD=${secretKey} + ''; }; + networking.firewall.allowedTCPPorts = [ 9000 ]; + }; - client = - { config, pkgs, ... }: - { virtualisation.writableStore = true; - nix.extraOptions = "experimental-features = nix-command"; - }; - }; + client = + { config, pkgs, ... }: + { + virtualisation.writableStore = true; + nix.extraOptions = '' + experimental-features = nix-command + substituters = + ''; + }; + }; + + testScript = + { nodes }: + '' + # fmt: off + start_all() + + # Create a binary cache. + server.wait_for_unit("minio") + server.wait_for_unit("network-addresses-eth1.service") + server.wait_for_open_port(9000) - testScript = { nodes }: '' - # fmt: off - start_all() + server.succeed("mc config host add minio http://localhost:9000 ${accessKey} ${secretKey} --api s3v4") + server.succeed("mc mb minio/my-cache") - # Create a binary cache. - server.wait_for_unit("minio") + server.succeed("${env} nix copy --to '${storeUrl}' ${pkgA}") - server.succeed("mc config host add minio http://localhost:9000 ${accessKey} ${secretKey} --api s3v4") - server.succeed("mc mb minio/my-cache") + client.wait_for_unit("network-addresses-eth1.service") - server.succeed("${env} nix copy --to '${storeUrl}' ${pkgA}") + # Test fetchurl on s3:// URLs while we're at it. + client.succeed("${env} nix eval --impure --expr 'builtins.fetchurl { name = \"foo\"; url = \"s3://my-cache/nix-cache-info?endpoint=http://server:9000®ion=eu-west-1\"; }'") - # Test fetchurl on s3:// URLs while we're at it. - client.succeed("${env} nix eval --impure --expr 'builtins.fetchurl { name = \"foo\"; url = \"s3://my-cache/nix-cache-info?endpoint=http://server:9000®ion=eu-west-1\"; }'") + # Test that the format string in the error message is properly setup and won't display `%s` instead of the failed URI + msg = client.fail("${env} nix eval --impure --expr 'builtins.fetchurl { name = \"foo\"; url = \"${objectThatDoesNotExist}\"; }' 2>&1") + if "S3 object '${objectThatDoesNotExist}' does not exist" not in msg: + print(msg) # So that you can see the message that was improperly formatted + raise Exception("Error message formatting didn't work") - # Copy a package from the binary cache. - client.fail("nix path-info ${pkgA}") + # Copy a package from the binary cache. + client.fail("nix path-info ${pkgA}") - client.succeed("${env} nix store info --store '${storeUrl}' >&2") + client.succeed("${env} nix store info --store '${storeUrl}' >&2") - client.succeed("${env} nix copy --no-check-sigs --from '${storeUrl}' ${pkgA}") + client.succeed("${env} nix copy --no-check-sigs --from '${storeUrl}' ${pkgA}") - client.succeed("nix path-info ${pkgA}") - ''; + client.succeed("nix path-info ${pkgA}") + ''; } diff --git a/tests/nixos/setuid.nix b/tests/nixos/setuid.nix index 2b66320ddaf..dc368e38373 100644 --- a/tests/nixos/setuid.nix +++ b/tests/nixos/setuid.nix @@ -1,6 +1,11 @@ # Verify that Linux builds cannot create setuid or setgid binaries. -{ lib, config, nixpkgs, ... }: +{ + lib, + config, + nixpkgs, + ... +}: let pkgs = config.nodes.machine.nixpkgs.pkgs; @@ -10,116 +15,127 @@ in name = "setuid"; nodes.machine = - { config, lib, pkgs, ... }: - { virtualisation.writableStore = true; + { + config, + lib, + pkgs, + ... + }: + { + virtualisation.writableStore = true; nix.settings.substituters = lib.mkForce [ ]; nix.nixPath = [ "nixpkgs=${lib.cleanSource pkgs.path}" ]; - virtualisation.additionalPaths = [ pkgs.stdenvNoCC pkgs.pkgsi686Linux.stdenvNoCC ]; + virtualisation.additionalPaths = [ + pkgs.stdenvNoCC + pkgs.pkgsi686Linux.stdenvNoCC + ]; }; - testScript = { nodes }: '' - # fmt: off - start_all() - - # Copying to /tmp should succeed. - machine.succeed(r""" - nix-build --no-sandbox -E '(with import {}; runCommand "foo" {} " - mkdir -p $out - cp ${pkgs.coreutils}/bin/id /tmp/id - ")' - """.strip()) - - machine.succeed('[[ $(stat -c %a /tmp/id) = 555 ]]') - - machine.succeed("rm /tmp/id") - - # Creating a setuid binary should fail. - machine.fail(r""" - nix-build --no-sandbox -E '(with import {}; runCommand "foo" {} " - mkdir -p $out - cp ${pkgs.coreutils}/bin/id /tmp/id - chmod 4755 /tmp/id - ")' - """.strip()) - - machine.succeed('[[ $(stat -c %a /tmp/id) = 555 ]]') - - machine.succeed("rm /tmp/id") - - # Creating a setgid binary should fail. - machine.fail(r""" - nix-build --no-sandbox -E '(with import {}; runCommand "foo" {} " - mkdir -p $out - cp ${pkgs.coreutils}/bin/id /tmp/id - chmod 2755 /tmp/id - ")' - """.strip()) - - machine.succeed('[[ $(stat -c %a /tmp/id) = 555 ]]') - - machine.succeed("rm /tmp/id") - - # The checks should also work on 32-bit binaries. - machine.fail(r""" - nix-build --no-sandbox -E '(with import { system = "i686-linux"; }; runCommand "foo" {} " - mkdir -p $out - cp ${pkgs.coreutils}/bin/id /tmp/id - chmod 2755 /tmp/id - ")' - """.strip()) - - machine.succeed('[[ $(stat -c %a /tmp/id) = 555 ]]') - - machine.succeed("rm /tmp/id") - - # The tests above use fchmodat(). Test chmod() as well. - machine.succeed(r""" - nix-build --no-sandbox -E '(with import {}; runCommand "foo" { buildInputs = [ perl ]; } " - mkdir -p $out - cp ${pkgs.coreutils}/bin/id /tmp/id - perl -e \"chmod 0666, qw(/tmp/id) or die\" - ")' - """.strip()) - - machine.succeed('[[ $(stat -c %a /tmp/id) = 666 ]]') - - machine.succeed("rm /tmp/id") - - machine.fail(r""" - nix-build --no-sandbox -E '(with import {}; runCommand "foo" { buildInputs = [ perl ]; } " - mkdir -p $out - cp ${pkgs.coreutils}/bin/id /tmp/id - perl -e \"chmod 04755, qw(/tmp/id) or die\" - ")' - """.strip()) - - machine.succeed('[[ $(stat -c %a /tmp/id) = 555 ]]') - - machine.succeed("rm /tmp/id") - - # And test fchmod(). - machine.succeed(r""" - nix-build --no-sandbox -E '(with import {}; runCommand "foo" { buildInputs = [ perl ]; } " - mkdir -p $out - cp ${pkgs.coreutils}/bin/id /tmp/id - perl -e \"my \\\$x; open \\\$x, qw(/tmp/id); chmod 01750, \\\$x or die\" - ")' - """.strip()) - - machine.succeed('[[ $(stat -c %a /tmp/id) = 1750 ]]') - - machine.succeed("rm /tmp/id") - - machine.fail(r""" - nix-build --no-sandbox -E '(with import {}; runCommand "foo" { buildInputs = [ perl ]; } " - mkdir -p $out - cp ${pkgs.coreutils}/bin/id /tmp/id - perl -e \"my \\\$x; open \\\$x, qw(/tmp/id); chmod 04777, \\\$x or die\" - ")' - """.strip()) - - machine.succeed('[[ $(stat -c %a /tmp/id) = 555 ]]') - - machine.succeed("rm /tmp/id") - ''; + testScript = + { nodes }: + '' + # fmt: off + start_all() + + # Copying to /tmp should succeed. + machine.succeed(r""" + nix-build --no-sandbox -E '(with import {}; runCommand "foo" {} " + mkdir -p $out + cp ${pkgs.coreutils}/bin/id /tmp/id + ")' + """.strip()) + + machine.succeed('[[ $(stat -c %a /tmp/id) = 555 ]]') + + machine.succeed("rm /tmp/id") + + # Creating a setuid binary should fail. + machine.fail(r""" + nix-build --no-sandbox -E '(with import {}; runCommand "foo" {} " + mkdir -p $out + cp ${pkgs.coreutils}/bin/id /tmp/id + chmod 4755 /tmp/id + ")' + """.strip()) + + machine.succeed('[[ $(stat -c %a /tmp/id) = 555 ]]') + + machine.succeed("rm /tmp/id") + + # Creating a setgid binary should fail. + machine.fail(r""" + nix-build --no-sandbox -E '(with import {}; runCommand "foo" {} " + mkdir -p $out + cp ${pkgs.coreutils}/bin/id /tmp/id + chmod 2755 /tmp/id + ")' + """.strip()) + + machine.succeed('[[ $(stat -c %a /tmp/id) = 555 ]]') + + machine.succeed("rm /tmp/id") + + # The checks should also work on 32-bit binaries. + machine.fail(r""" + nix-build --no-sandbox -E '(with import { system = "i686-linux"; }; runCommand "foo" {} " + mkdir -p $out + cp ${pkgs.coreutils}/bin/id /tmp/id + chmod 2755 /tmp/id + ")' + """.strip()) + + machine.succeed('[[ $(stat -c %a /tmp/id) = 555 ]]') + + machine.succeed("rm /tmp/id") + + # The tests above use fchmodat(). Test chmod() as well. + machine.succeed(r""" + nix-build --no-sandbox -E '(with import {}; runCommand "foo" { buildInputs = [ perl ]; } " + mkdir -p $out + cp ${pkgs.coreutils}/bin/id /tmp/id + perl -e \"chmod 0666, qw(/tmp/id) or die\" + ")' + """.strip()) + + machine.succeed('[[ $(stat -c %a /tmp/id) = 666 ]]') + + machine.succeed("rm /tmp/id") + + machine.fail(r""" + nix-build --no-sandbox -E '(with import {}; runCommand "foo" { buildInputs = [ perl ]; } " + mkdir -p $out + cp ${pkgs.coreutils}/bin/id /tmp/id + perl -e \"chmod 04755, qw(/tmp/id) or die\" + ")' + """.strip()) + + machine.succeed('[[ $(stat -c %a /tmp/id) = 555 ]]') + + machine.succeed("rm /tmp/id") + + # And test fchmod(). + machine.succeed(r""" + nix-build --no-sandbox -E '(with import {}; runCommand "foo" { buildInputs = [ perl ]; } " + mkdir -p $out + cp ${pkgs.coreutils}/bin/id /tmp/id + perl -e \"my \\\$x; open \\\$x, qw(/tmp/id); chmod 01750, \\\$x or die\" + ")' + """.strip()) + + machine.succeed('[[ $(stat -c %a /tmp/id) = 1750 ]]') + + machine.succeed("rm /tmp/id") + + machine.fail(r""" + nix-build --no-sandbox -E '(with import {}; runCommand "foo" { buildInputs = [ perl ]; } " + mkdir -p $out + cp ${pkgs.coreutils}/bin/id /tmp/id + perl -e \"my \\\$x; open \\\$x, qw(/tmp/id); chmod 04777, \\\$x or die\" + ")' + """.strip()) + + machine.succeed('[[ $(stat -c %a /tmp/id) = 555 ]]') + + machine.succeed("rm /tmp/id") + ''; } diff --git a/tests/nixos/sourcehut-flakes.nix b/tests/nixos/sourcehut-flakes.nix index 04f3590e1d8..3f05130d6aa 100644 --- a/tests/nixos/sourcehut-flakes.nix +++ b/tests/nixos/sourcehut-flakes.nix @@ -1,22 +1,27 @@ -{ lib, config, hostPkgs, nixpkgs, ... }: +{ + lib, + config, + hostPkgs, + nixpkgs, + ... +}: let pkgs = config.nodes.sourcehut.nixpkgs.pkgs; # Generate a fake root CA and a fake git.sr.ht certificate. - cert = pkgs.runCommand "cert" { buildInputs = [ pkgs.openssl ]; } - '' - mkdir -p $out + cert = pkgs.runCommand "cert" { buildInputs = [ pkgs.openssl ]; } '' + mkdir -p $out - openssl genrsa -out ca.key 2048 - openssl req -new -x509 -days 36500 -key ca.key \ - -subj "/C=NL/ST=Denial/L=Springfield/O=Dis/CN=Root CA" -out $out/ca.crt + openssl genrsa -out ca.key 2048 + openssl req -new -x509 -days 36500 -key ca.key \ + -subj "/C=NL/ST=Denial/L=Springfield/O=Dis/CN=Root CA" -out $out/ca.crt - openssl req -newkey rsa:2048 -nodes -keyout $out/server.key \ - -subj "/C=CN/ST=Denial/L=Springfield/O=Dis/CN=git.sr.ht" -out server.csr - openssl x509 -req -extfile <(printf "subjectAltName=DNS:git.sr.ht") \ - -days 36500 -in server.csr -CA $out/ca.crt -CAkey ca.key -CAcreateserial -out $out/server.crt - ''; + openssl req -newkey rsa:2048 -nodes -keyout $out/server.key \ + -subj "/C=CN/ST=Denial/L=Springfield/O=Dis/CN=git.sr.ht" -out server.csr + openssl x509 -req -extfile <(printf "subjectAltName=DNS:git.sr.ht") \ + -days 36500 -in server.csr -CA $out/ca.crt -CAkey ca.key -CAcreateserial -out $out/server.crt + ''; registry = pkgs.writeTextFile { name = "registry"; @@ -41,80 +46,92 @@ let destination = "/flake-registry.json"; }; - nixpkgs-repo = pkgs.runCommand "nixpkgs-flake" { } - '' - dir=NixOS-nixpkgs-${nixpkgs.shortRev} - cp -prd ${nixpkgs} $dir + nixpkgs-repo = pkgs.runCommand "nixpkgs-flake" { } '' + dir=NixOS-nixpkgs-${nixpkgs.shortRev} + cp -rd --preserve=ownership,timestamps ${nixpkgs} $dir - # Set the correct timestamp in the tarball. - find $dir -print0 | xargs -0 touch -h -t ${builtins.substring 0 12 nixpkgs.lastModifiedDate}.${builtins.substring 12 2 nixpkgs.lastModifiedDate} -- + # Set the correct timestamp in the tarball. + find $dir -print0 | xargs -0 touch -h -t ${builtins.substring 0 12 nixpkgs.lastModifiedDate}.${ + builtins.substring 12 2 nixpkgs.lastModifiedDate + } -- - mkdir -p $out/archive - tar cfz $out/archive/${nixpkgs.rev}.tar.gz $dir --hard-dereference + mkdir -p $out/archive + tar cfz $out/archive/${nixpkgs.rev}.tar.gz $dir --hard-dereference - echo 'ref: refs/heads/master' > $out/HEAD + echo 'ref: refs/heads/master' > $out/HEAD - mkdir -p $out/info - echo -e '${nixpkgs.rev}\trefs/heads/master\n${nixpkgs.rev}\trefs/tags/foo-bar' > $out/info/refs - ''; + mkdir -p $out/info + echo -e '${nixpkgs.rev}\trefs/heads/master\n${nixpkgs.rev}\trefs/tags/foo-bar' > $out/info/refs + ''; in - { - name = "sourcehut-flakes"; +{ + name = "sourcehut-flakes"; - nodes = + nodes = { + # Impersonate git.sr.ht + sourcehut = + { config, pkgs, ... }: { - # Impersonate git.sr.ht - sourcehut = - { config, pkgs, ... }: - { - networking.firewall.allowedTCPPorts = [ 80 443 ]; - - services.httpd.enable = true; - services.httpd.adminAddr = "foo@example.org"; - services.httpd.extraConfig = '' - ErrorLog syslog:local6 - ''; - services.httpd.virtualHosts."git.sr.ht" = - { - forceSSL = true; - sslServerKey = "${cert}/server.key"; - sslServerCert = "${cert}/server.crt"; - servedDirs = - [ - { - urlPath = "/~NixOS/nixpkgs"; - dir = nixpkgs-repo; - } - { - urlPath = "/~NixOS/flake-registry/blob/master"; - dir = registry; - } - ]; - }; - }; - - client = - { config, lib, pkgs, nodes, ... }: - { - virtualisation.writableStore = true; - virtualisation.diskSize = 2048; - virtualisation.additionalPaths = [ pkgs.hello pkgs.fuse ]; - virtualisation.memorySize = 4096; - nix.settings.substituters = lib.mkForce [ ]; - nix.extraOptions = '' - experimental-features = nix-command flakes - flake-registry = https://git.sr.ht/~NixOS/flake-registry/blob/master/flake-registry.json - ''; - environment.systemPackages = [ pkgs.jq ]; - networking.hosts.${(builtins.head nodes.sourcehut.networking.interfaces.eth1.ipv4.addresses).address} = - [ "git.sr.ht" ]; - security.pki.certificateFiles = [ "${cert}/ca.crt" ]; - }; + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; + + services.httpd.enable = true; + services.httpd.adminAddr = "foo@example.org"; + services.httpd.extraConfig = '' + ErrorLog syslog:local6 + ''; + services.httpd.virtualHosts."git.sr.ht" = { + forceSSL = true; + sslServerKey = "${cert}/server.key"; + sslServerCert = "${cert}/server.crt"; + servedDirs = [ + { + urlPath = "/~NixOS/nixpkgs"; + dir = nixpkgs-repo; + } + { + urlPath = "/~NixOS/flake-registry/blob/master"; + dir = registry; + } + ]; + }; }; - testScript = { nodes }: '' + client = + { + config, + lib, + pkgs, + nodes, + ... + }: + { + virtualisation.writableStore = true; + virtualisation.diskSize = 2048; + virtualisation.additionalPaths = [ + pkgs.hello + pkgs.fuse + ]; + virtualisation.memorySize = 4096; + nix.settings.substituters = lib.mkForce [ ]; + nix.extraOptions = '' + experimental-features = nix-command flakes + flake-registry = https://git.sr.ht/~NixOS/flake-registry/blob/master/flake-registry.json + ''; + environment.systemPackages = [ pkgs.jq ]; + networking.hosts.${(builtins.head nodes.sourcehut.networking.interfaces.eth1.ipv4.addresses).address} = + [ "git.sr.ht" ]; + security.pki.certificateFiles = [ "${cert}/ca.crt" ]; + }; + }; + + testScript = + { nodes }: + '' # fmt: off import json import time @@ -122,6 +139,8 @@ in start_all() sourcehut.wait_for_unit("httpd.service") + sourcehut.wait_for_unit("network-addresses-eth1.service") + client.wait_for_unit("network-addresses-eth1.service") client.succeed("curl -v https://git.sr.ht/ >&2") client.succeed("nix registry list | grep nixpkgs") diff --git a/tests/nixos/tarball-flakes.nix b/tests/nixos/tarball-flakes.nix index 84cf377ec5b..26c20cb1aef 100644 --- a/tests/nixos/tarball-flakes.nix +++ b/tests/nixos/tarball-flakes.nix @@ -1,94 +1,106 @@ -{ lib, config, nixpkgs, ... }: +{ + lib, + config, + nixpkgs, + ... +}: let pkgs = config.nodes.machine.nixpkgs.pkgs; - root = pkgs.runCommand "nixpkgs-flake" {} - '' - mkdir -p $out/{stable,tags} - - set -x - dir=nixpkgs-${nixpkgs.shortRev} - cp -prd ${nixpkgs} $dir - # Set the correct timestamp in the tarball. - find $dir -print0 | xargs -0 touch -h -t ${builtins.substring 0 12 nixpkgs.lastModifiedDate}.${builtins.substring 12 2 nixpkgs.lastModifiedDate} -- - tar cfz $out/stable/${nixpkgs.rev}.tar.gz $dir --hard-dereference - - # Set the "Link" header on the redirect but not the final response to - # simulate an S3-like serving environment where the final host cannot set - # arbitrary headers. - cat >$out/tags/.htaccess <; rel=\"immutable\"" - EOF - ''; + root = pkgs.runCommand "nixpkgs-flake" { } '' + mkdir -p $out/{stable,tags} + + set -x + dir=nixpkgs-${nixpkgs.shortRev} + cp -rd --preserve=ownership,timestamps ${nixpkgs} $dir + # Set the correct timestamp in the tarball. + find $dir -print0 | xargs -0 touch -h -t ${builtins.substring 0 12 nixpkgs.lastModifiedDate}.${ + builtins.substring 12 2 nixpkgs.lastModifiedDate + } -- + tar cfz $out/stable/${nixpkgs.rev}.tar.gz $dir --hard-dereference + + # Set the "Link" header on the redirect but not the final response to + # simulate an S3-like serving environment where the final host cannot set + # arbitrary headers. + cat >$out/tags/.htaccess <; rel=\"immutable\"" + EOF + ''; in { name = "tarball-flakes"; - nodes = - { - machine = - { config, pkgs, ... }: - { networking.firewall.allowedTCPPorts = [ 80 ]; - - services.httpd.enable = true; - services.httpd.adminAddr = "foo@example.org"; - services.httpd.extraConfig = '' - ErrorLog syslog:local6 - ''; - services.httpd.virtualHosts."localhost" = - { servedDirs = - [ { urlPath = "/"; - dir = root; - } - ]; - }; - - virtualisation.writableStore = true; - virtualisation.diskSize = 2048; - virtualisation.additionalPaths = [ pkgs.hello pkgs.fuse ]; - virtualisation.memorySize = 4096; - nix.settings.substituters = lib.mkForce [ ]; - nix.extraOptions = "experimental-features = nix-command flakes"; + nodes = { + machine = + { config, pkgs, ... }: + { + networking.firewall.allowedTCPPorts = [ 80 ]; + + services.httpd.enable = true; + services.httpd.adminAddr = "foo@example.org"; + services.httpd.extraConfig = '' + ErrorLog syslog:local6 + ''; + services.httpd.virtualHosts."localhost" = { + servedDirs = [ + { + urlPath = "/"; + dir = root; + } + ]; }; - }; - testScript = { nodes }: '' - # fmt: off - import json + virtualisation.writableStore = true; + virtualisation.diskSize = 2048; + virtualisation.additionalPaths = [ + pkgs.hello + pkgs.fuse + ]; + virtualisation.memorySize = 4096; + nix.settings.substituters = lib.mkForce [ ]; + nix.extraOptions = "experimental-features = nix-command flakes"; + }; + }; + + testScript = + { nodes }: + '' + # fmt: off + import json - start_all() + start_all() - machine.wait_for_unit("httpd.service") + machine.wait_for_unit("httpd.service") - out = machine.succeed("nix flake metadata --json http://localhost/tags/latest.tar.gz") - print(out) - info = json.loads(out) + out = machine.succeed("nix flake metadata --json http://localhost/tags/latest.tar.gz") + print(out) + info = json.loads(out) - # Check that we got redirected to the immutable URL. - assert info["locked"]["url"] == "http://localhost/stable/${nixpkgs.rev}.tar.gz" + # Check that we got redirected to the immutable URL. + assert info["locked"]["url"] == "http://localhost/stable/${nixpkgs.rev}.tar.gz" - # Check that we got a fingerprint for caching. - assert info["fingerprint"] + # Check that we got a fingerprint for caching. + assert info["fingerprint"] - # Check that we got the rev and revCount attributes. - assert info["revision"] == "${nixpkgs.rev}" - assert info["revCount"] == 1234 + # Check that we got the rev and revCount attributes. + assert info["revision"] == "${nixpkgs.rev}" + assert info["revCount"] == 1234 - # Check that a 0-byte HTTP 304 "Not modified" result works. - machine.succeed("nix flake metadata --refresh --json http://localhost/tags/latest.tar.gz") + # Check that a 0-byte HTTP 304 "Not modified" result works. + machine.succeed("nix flake metadata --refresh --json http://localhost/tags/latest.tar.gz") - # Check that fetching with rev/revCount/narHash succeeds. - machine.succeed("nix flake metadata --json http://localhost/tags/latest.tar.gz?rev=" + info["revision"]) - machine.succeed("nix flake metadata --json http://localhost/tags/latest.tar.gz?revCount=" + str(info["revCount"])) - machine.succeed("nix flake metadata --json http://localhost/tags/latest.tar.gz?narHash=" + info["locked"]["narHash"]) + # Check that fetching with rev/revCount/narHash succeeds. + machine.succeed("nix flake metadata --json http://localhost/tags/latest.tar.gz?rev=" + info["revision"]) + machine.succeed("nix flake metadata --json http://localhost/tags/latest.tar.gz?revCount=" + str(info["revCount"])) + machine.succeed("nix flake metadata --json http://localhost/tags/latest.tar.gz?narHash=" + info["locked"]["narHash"]) - # Check that fetching fails if we provide incorrect attributes. - machine.fail("nix flake metadata --json http://localhost/tags/latest.tar.gz?rev=493300eb13ae6fb387fbd47bf54a85915acc31c0") - machine.fail("nix flake metadata --json http://localhost/tags/latest.tar.gz?revCount=789") - machine.fail("nix flake metadata --json http://localhost/tags/latest.tar.gz?narHash=sha256-tbudgBSg+bHWHiHnlteNzN8TUvI80ygS9IULh4rklEw=") - ''; + # Check that fetching fails if we provide incorrect attributes. + machine.fail("nix flake metadata --json http://localhost/tags/latest.tar.gz?rev=493300eb13ae6fb387fbd47bf54a85915acc31c0") + machine.fail("nix flake metadata --json http://localhost/tags/latest.tar.gz?revCount=789") + machine.fail("nix flake metadata --json http://localhost/tags/latest.tar.gz?narHash=sha256-tbudgBSg+bHWHiHnlteNzN8TUvI80ygS9IULh4rklEw=") + ''; } diff --git a/tests/nixos/user-sandboxing/default.nix b/tests/nixos/user-sandboxing/default.nix index 8a16f44e84d..3f6b575b035 100644 --- a/tests/nixos/user-sandboxing/default.nix +++ b/tests/nixos/user-sandboxing/default.nix @@ -3,12 +3,15 @@ let pkgs = config.nodes.machine.nixpkgs.pkgs; - attacker = pkgs.runCommandWith { - name = "attacker"; - stdenv = pkgs.pkgsStatic.stdenv; - } '' - $CC -static -o $out ${./attacker.c} - ''; + attacker = + pkgs.runCommandWith + { + name = "attacker"; + stdenv = pkgs.pkgsStatic.stdenv; + } + '' + $CC -static -o $out ${./attacker.c} + ''; try-open-build-dir = pkgs.writeScript "try-open-build-dir" '' export PATH=${pkgs.coreutils}/bin:$PATH @@ -55,75 +58,90 @@ in name = "sandbox-setuid-leak"; nodes.machine = - { config, lib, pkgs, ... }: - { virtualisation.writableStore = true; + { + config, + lib, + pkgs, + ... + }: + { + virtualisation.writableStore = true; nix.settings.substituters = lib.mkForce [ ]; nix.nrBuildUsers = 1; - virtualisation.additionalPaths = [ pkgs.busybox-sandbox-shell attacker try-open-build-dir create-hello-world pkgs.socat ]; + virtualisation.additionalPaths = [ + pkgs.busybox-sandbox-shell + attacker + try-open-build-dir + create-hello-world + pkgs.socat + ]; boot.kernelPackages = pkgs.linuxPackages_latest; users.users.alice = { isNormalUser = true; }; }; - testScript = { nodes }: '' - start_all() - - with subtest("A builder can't give access to its build directory"): - # Make sure that a builder can't change the permissions on its build - # directory to the point of opening it up to external users - - # A derivation whose builder tries to make its build directory as open - # as possible and wait for someone to hijack it - machine.succeed(r""" - nix-build -v -E ' - builtins.derivation { - name = "open-build-dir"; - system = builtins.currentSystem; - builder = "${pkgs.busybox-sandbox-shell}/bin/sh"; - args = [ (builtins.storePath "${try-open-build-dir}") ]; - }' >&2 & - """.strip()) - - # Wait for the build to be ready - # This is OK because it runs as root, so we can access everything - machine.wait_for_file("/tmp/nix-build-open-build-dir.drv-0/build/syncPoint") - - # But Alice shouldn't be able to access the build directory - machine.fail("su alice -c 'ls /tmp/nix-build-open-build-dir.drv-0/build'") - machine.fail("su alice -c 'touch /tmp/nix-build-open-build-dir.drv-0/build/bar'") - machine.fail("su alice -c 'cat /tmp/nix-build-open-build-dir.drv-0/build/foo'") - - # Tell the user to finish the build - machine.succeed("echo foo > /tmp/nix-build-open-build-dir.drv-0/build/syncPoint") - - with subtest("Being able to execute stuff as the build user doesn't give access to the build dir"): - machine.succeed(r""" - nix-build -E ' - builtins.derivation { - name = "innocent"; - system = builtins.currentSystem; - builder = "${pkgs.busybox-sandbox-shell}/bin/sh"; - args = [ (builtins.storePath "${create-hello-world}") ]; - }' >&2 & - """.strip()) - machine.wait_for_file("/tmp/nix-build-innocent.drv-0/build/syncPoint") - - # The build ran as `nixbld1` (which is the only build user on the - # machine), but a process running as `nixbld1` outside the sandbox - # shouldn't be able to touch the build directory regardless - machine.fail("su nixbld1 --shell ${pkgs.busybox-sandbox-shell}/bin/sh -c 'ls /tmp/nix-build-innocent.drv-0/build'") - machine.fail("su nixbld1 --shell ${pkgs.busybox-sandbox-shell}/bin/sh -c 'echo pwned > /tmp/nix-build-innocent.drv-0/build/result'") - - # Finish the build - machine.succeed("echo foo > /tmp/nix-build-innocent.drv-0/build/syncPoint") - - # Check that the build was not affected - machine.succeed(r""" - cat ./result - test "$(cat ./result)" = "hello, world" - """.strip()) - ''; + testScript = + { nodes }: + '' + start_all() + + with subtest("A builder can't give access to its build directory"): + # Make sure that a builder can't change the permissions on its build + # directory to the point of opening it up to external users + + # A derivation whose builder tries to make its build directory as open + # as possible and wait for someone to hijack it + machine.succeed(r""" + nix-build -v -E ' + builtins.derivation { + name = "open-build-dir"; + system = builtins.currentSystem; + builder = "${pkgs.busybox-sandbox-shell}/bin/sh"; + args = [ (builtins.storePath "${try-open-build-dir}") ]; + }' >&2 & + """.strip()) + + # Wait for the build to be ready + # This is OK because it runs as root, so we can access everything + machine.wait_until_succeeds("stat /nix/var/nix/builds/nix-build-open-build-dir.drv-*/build/syncPoint") + dir = machine.succeed("ls -d /nix/var/nix/builds/nix-build-open-build-dir.drv-*").strip() + + # But Alice shouldn't be able to access the build directory + machine.fail(f"su alice -c 'ls {dir}/build'") + machine.fail(f"su alice -c 'touch {dir}/build/bar'") + machine.fail(f"su alice -c 'cat {dir}/build/foo'") + + # Tell the user to finish the build + machine.succeed(f"echo foo > {dir}/build/syncPoint") + + with subtest("Being able to execute stuff as the build user doesn't give access to the build dir"): + machine.succeed(r""" + nix-build -E ' + builtins.derivation { + name = "innocent"; + system = builtins.currentSystem; + builder = "${pkgs.busybox-sandbox-shell}/bin/sh"; + args = [ (builtins.storePath "${create-hello-world}") ]; + }' >&2 & + """.strip()) + machine.wait_until_succeeds("stat /nix/var/nix/builds/nix-build-innocent.drv-*/build/syncPoint") + dir = machine.succeed("ls -d /nix/var/nix/builds/nix-build-innocent.drv-*").strip() + + # The build ran as `nixbld1` (which is the only build user on the + # machine), but a process running as `nixbld1` outside the sandbox + # shouldn't be able to touch the build directory regardless + machine.fail(f"su nixbld1 --shell ${pkgs.busybox-sandbox-shell}/bin/sh -c 'ls {dir}/build'") + machine.fail(f"su nixbld1 --shell ${pkgs.busybox-sandbox-shell}/bin/sh -c 'echo pwned > {dir}/build/result'") + + # Finish the build + machine.succeed(f"echo foo > {dir}/build/syncPoint") + + # Check that the build was not affected + machine.succeed(r""" + cat ./result + test "$(cat ./result)" = "hello, world" + """.strip()) + ''; } - diff --git a/tests/repl-completion.nix b/tests/repl-completion.nix index 3ba198a9860..07406e969cd 100644 --- a/tests/repl-completion.nix +++ b/tests/repl-completion.nix @@ -1,40 +1,45 @@ -{ runCommand, nix, expect }: +{ + runCommand, + nix, + expect, +}: # We only use expect when necessary, e.g. for testing tab completion in nix repl. # See also tests/functional/repl.sh -runCommand "repl-completion" { - nativeBuildInputs = [ - expect - nix - ]; - expectScript = '' - # Regression https://github.com/NixOS/nix/pull/10778 - spawn nix repl --offline --extra-experimental-features nix-command - expect "nix-repl>" - send "foo = import ./does-not-exist.nix\n" - expect "nix-repl>" - send "foo.\t" - expect { - "nix-repl>" { - puts "Got another prompt. Good." +runCommand "repl-completion" + { + nativeBuildInputs = [ + expect + nix + ]; + expectScript = '' + # Regression https://github.com/NixOS/nix/pull/10778 + spawn nix repl --offline --extra-experimental-features nix-command + expect "nix-repl>" + send "foo = import ./does-not-exist.nix\n" + expect "nix-repl>" + send "foo.\t" + expect { + "nix-repl>" { + puts "Got another prompt. Good." + } + eof { + puts "Got EOF. Bad." + exit 1 + } } - eof { - puts "Got EOF. Bad." - exit 1 - } - } - exit 0 - ''; - passAsFile = [ "expectScript" ]; -} -'' - export NIX_STORE=$TMPDIR/store - export NIX_STATE_DIR=$TMPDIR/state - export HOME=$TMPDIR/home - mkdir $HOME + exit 0 + ''; + passAsFile = [ "expectScript" ]; + } + '' + export NIX_STORE=$TMPDIR/store + export NIX_STATE_DIR=$TMPDIR/state + export HOME=$TMPDIR/home + mkdir $HOME - nix-store --init - expect $expectScriptPath - touch $out -'' \ No newline at end of file + nix-store --init + expect $expectScriptPath + touch $out + ''