diff --git a/.github/workflows/build_cross.yml b/.github/workflows/build_cross.yml new file mode 100644 index 00000000..13a53278 --- /dev/null +++ b/.github/workflows/build_cross.yml @@ -0,0 +1,100 @@ +name: Cross-compilation build + +on: + push: + branches: [develop] + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + shell: + name: Initialize devshell + runs-on: ubuntu-24.04-arm + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 1 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v22 + with: + extra-conf: | + extra-substituters = https://dashpay-base-sdk.cachix.org + extra-trusted-public-keys = dashpay-base-sdk.cachix.org-1:TAoDyYL2e60lUEG8n10gvIZmujM5rJAhaG613fNfOWI= + + - name: Cache Nix + uses: cachix/cachix-action@v17 + with: + name: dashpay-base-sdk + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + skipAddingSubstituter: true + + - name: Initialize devshell + run: nix develop ./contrib/nix#dev --command true + + - name: Validate toolchains + run: nix develop ./contrib/nix#dev --command ./contrib/nix/smoke_test.sh + + build: + name: ${{ matrix.target }} + runs-on: ubuntu-24.04-arm + needs: shell + strategy: + fail-fast: false + matrix: + target: + - x86_64-unknown-linux-gnu + - x86_64-pc-windows-gnu + - aarch64-apple-darwin + + defaults: + run: + shell: nix develop ./contrib/nix#dev --command bash -eo pipefail {0} + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 1 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v22 + with: + extra-conf: | + extra-substituters = https://dashpay-base-sdk.cachix.org + extra-trusted-public-keys = dashpay-base-sdk.cachix.org-1:TAoDyYL2e60lUEG8n10gvIZmujM5rJAhaG613fNfOWI= + + - name: Cache Nix + uses: cachix/cachix-action@v17 + with: + name: dashpay-base-sdk + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + skipAddingSubstituter: true + + - name: Restore cargo registry + uses: actions/cache/restore@v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: cargo-deps-${{ hashFiles('Cargo.lock', 'docs/samples/Cargo.lock') }} + restore-keys: cargo-deps- + + - name: Manage build artifacts + uses: actions/cache@v5 + with: + path: target + key: cargo-build-cross-${{ matrix.target }}-${{ github.sha }} + restore-keys: | + cargo-build-cross-${{ matrix.target }}- + + - name: Build workspace + run: cargo build --workspace --all-targets --features full --target ${{ matrix.target }} diff --git a/.github/workflows/build_msrv.yml b/.github/workflows/build_msrv.yml index c09c35f5..d437b794 100644 --- a/.github/workflows/build_msrv.yml +++ b/.github/workflows/build_msrv.yml @@ -17,6 +17,10 @@ jobs: name: Lint runs-on: ubuntu-24.04 + defaults: + run: + shell: nix develop ./contrib/nix#ci --command bash -eo pipefail {0} + steps: - name: Checkout uses: actions/checkout@v6 @@ -34,42 +38,19 @@ jobs: exit 1 fi - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@1.85.0 - with: - components: rustfmt - - - name: Set up Node.js - uses: actions/setup-node@v5 + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v22 with: - node-version: 24 + extra-conf: | + extra-substituters = https://dashpay-base-sdk.cachix.org + extra-trusted-public-keys = dashpay-base-sdk.cachix.org-1:TAoDyYL2e60lUEG8n10gvIZmujM5rJAhaG613fNfOWI= - - name: Set up Python - id: python - uses: actions/setup-python@v6 + - name: Cache Nix + uses: cachix/cachix-action@v17 with: - python-version-file: pyproject.toml - - - name: Set up uv - uses: astral-sh/setup-uv@v10.0.1 - with: - version: 0.12.9 - enable-cache: true - cache-dependency-glob: uv.lock - - - name: Install Python dependencies - run: | - uv sync --locked --extra dev --python '${{ steps.python.outputs.python-path }}' - echo "${PWD}/.venv/bin" >> "${GITHUB_PATH}" - - - name: Install CodeQL - id: setup-codeql - uses: github/codeql-action/setup-codeql@v4.37.1 - with: - tools: linked - - - name: Setup CodeQL - run: echo "$(dirname '${{ steps.setup-codeql.outputs.codeql-path }}')" >> "$GITHUB_PATH" + name: dashpay-base-sdk + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + skipAddingSubstituter: true - name: Restore cargo registry uses: actions/cache/restore@v5 @@ -80,14 +61,6 @@ jobs: key: cargo-deps-${{ hashFiles('Cargo.lock', 'docs/samples/Cargo.lock') }} restore-keys: cargo-deps- - - name: Restore build artifacts - uses: actions/cache/restore@v5 - with: - path: target - key: cargo-build-msrv-${{ runner.os }}-${{ runner.arch }}-${{ github.sha }} - restore-keys: | - cargo-build-msrv-${{ runner.os }}-${{ runner.arch }}- - - name: Manage CodeQL packs uses: actions/cache@v5 with: @@ -98,13 +71,9 @@ jobs: run: | python3 maint/lint_all.py --exclude lint_codeql python3 maint/lint/lint_codeql.py check - env: - RUSTUP_TOOLCHAIN: 1.85.0 - name: Run CodeQL run: python3 maint/lint/lint_codeql.py run --lang=rust --with-suite=rust-security-and-quality - env: - RUSTUP_TOOLCHAIN: 1.85.0 - name: Check PR commit messages if: github.event_name == 'pull_request' @@ -116,6 +85,10 @@ jobs: name: Build and test runs-on: ubuntu-24.04-arm + defaults: + run: + shell: nix develop ./contrib/nix#ci --command bash -eo pipefail {0} + steps: - name: Checkout uses: actions/checkout@v6 @@ -133,8 +106,19 @@ jobs: exit 1 fi - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@1.85.0 + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v22 + with: + extra-conf: | + extra-substituters = https://dashpay-base-sdk.cachix.org + extra-trusted-public-keys = dashpay-base-sdk.cachix.org-1:TAoDyYL2e60lUEG8n10gvIZmujM5rJAhaG613fNfOWI= + + - name: Cache Nix + uses: cachix/cachix-action@v17 + with: + name: dashpay-base-sdk + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + skipAddingSubstituter: true - name: Restore cargo registry uses: actions/cache/restore@v5 @@ -155,14 +139,14 @@ jobs: - name: Build workspace run: | - cargo build --workspace --exclude dash-pow --features full - cargo build -p dash-pow --features std,aes_hw - env: - RUSTUP_TOOLCHAIN: 1.85.0 + export RUSTC="$TOOLCHAIN_MSRV/bin/rustc" + export RUSTDOC="$TOOLCHAIN_MSRV/bin/rustdoc" + "$TOOLCHAIN_MSRV/bin/cargo" build --workspace --exclude dash-pow --features full + "$TOOLCHAIN_MSRV/bin/cargo" build -p dash-pow --features std,aes_hw - name: Test workspace run: | - cargo test --workspace --exclude dash-pow --features full - cargo test -p dash-pow --features std,aes_hw - env: - RUSTUP_TOOLCHAIN: 1.85.0 + export RUSTC="$TOOLCHAIN_MSRV/bin/rustc" + export RUSTDOC="$TOOLCHAIN_MSRV/bin/rustdoc" + "$TOOLCHAIN_MSRV/bin/cargo" test --workspace --exclude dash-pow --features full + "$TOOLCHAIN_MSRV/bin/cargo" test -p dash-pow --features std,aes_hw diff --git a/.github/workflows/build_nightly.yml b/.github/workflows/build_nightly.yml index 3d11fb3a..a9d3a8a9 100644 --- a/.github/workflows/build_nightly.yml +++ b/.github/workflows/build_nightly.yml @@ -12,6 +12,8 @@ on: required: true type: string secrets: + CACHIX_AUTH_TOKEN: + required: false CODECOV_TOKEN: required: false @@ -32,6 +34,10 @@ jobs: - { name: min, args: "--no-default-features" } - { name: std, args: "--features std" } + defaults: + run: + shell: nix develop ./contrib/nix#ci --command bash -eo pipefail {0} + steps: - name: Checkout uses: actions/checkout@v6 @@ -49,11 +55,19 @@ jobs: exit 1 fi - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v22 + with: + extra-conf: | + extra-substituters = https://dashpay-base-sdk.cachix.org + extra-trusted-public-keys = dashpay-base-sdk.cachix.org-1:TAoDyYL2e60lUEG8n10gvIZmujM5rJAhaG613fNfOWI= + + - name: Cache Nix + uses: cachix/cachix-action@v17 with: - toolchain: nightly-2026-02-01 - components: clippy, llvm-tools, rustfmt + name: dashpay-base-sdk + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + skipAddingSubstituter: true - name: Manage cargo registry uses: actions/cache@v5 @@ -82,13 +96,9 @@ jobs: if: matrix.config.name != 'full' run: cargo test -p ${{ inputs.package }} ${{ matrix.config.args }} - - name: Install cargo-llvm-cov - if: matrix.config.name == 'full' - uses: taiki-e/install-action@cargo-llvm-cov - - name: Check formatting if: matrix.config.name == 'full' - run: python maint/lint/lint_rust.py + run: python3 maint/lint/lint_rust.py - name: Test package (with coverage) if: matrix.config.name == 'full' diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 2afb1ea4..362b53b6 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -10,8 +10,10 @@ on: - .github/workflows/pages.yml - pyproject.toml - uv.lock + - contrib/nix/** + - rust-toolchain.toml - README.md - - '**/README.md' + - "**/README.md" workflow_dispatch: concurrency: @@ -26,6 +28,10 @@ jobs: name: Build site runs-on: ubuntu-24.04-arm + defaults: + run: + shell: nix develop ./contrib/nix#ci --command bash -eo pipefail {0} + steps: - name: Checkout uses: actions/checkout@v6 @@ -33,32 +39,19 @@ jobs: fetch-depth: 1 persist-credentials: false - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v22 with: - toolchain: nightly-2026-02-01 - targets: wasm32-unknown-unknown - - - name: Install wasm-pack - run: cargo install wasm-pack@0.15.0 + extra-conf: | + extra-substituters = https://dashpay-base-sdk.cachix.org + extra-trusted-public-keys = dashpay-base-sdk.cachix.org-1:TAoDyYL2e60lUEG8n10gvIZmujM5rJAhaG613fNfOWI= - - name: Set up Python - id: python - uses: actions/setup-python@v6 + - name: Cache Nix + uses: cachix/cachix-action@v17 with: - python-version-file: pyproject.toml - - - name: Set up uv - uses: astral-sh/setup-uv@v10.0.1 - with: - version: 0.12.9 - enable-cache: true - cache-dependency-glob: uv.lock - - - name: Install Python dependencies - run: | - uv sync --locked --extra dev --python '${{ steps.python.outputs.python-path }}' - echo "${PWD}/.venv/bin" >> "${GITHUB_PATH}" + name: dashpay-base-sdk + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + skipAddingSubstituter: true - name: Test documentation tooling run: pytest @@ -73,7 +66,7 @@ jobs: restore-keys: cargo-deps- - name: Build documentation - run: python docs/build_docs.py build + run: python3 docs/build_docs.py build - name: Upload Pages artifact uses: actions/upload-pages-artifact@v5 diff --git a/.github/workflows/pkg_num.yml b/.github/workflows/pkg_num.yml index 2023c8c3..a399bc32 100644 --- a/.github/workflows/pkg_num.yml +++ b/.github/workflows/pkg_num.yml @@ -10,6 +10,10 @@ on: - .github/workflows/pkg_num.yml - .github/workflows/build_stable.yml - .github/workflows/build_nightly.yml + - contrib/nix/** + - pyproject.toml + - rust-toolchain.toml + - uv.lock pull_request: paths: *paths diff --git a/.github/workflows/pkg_p2p_core.yml b/.github/workflows/pkg_p2p_core.yml index 8bda0071..dece3458 100644 --- a/.github/workflows/pkg_p2p_core.yml +++ b/.github/workflows/pkg_p2p_core.yml @@ -15,6 +15,10 @@ on: - .github/workflows/pkg_p2p_core.yml - .github/workflows/build_stable.yml - .github/workflows/build_nightly.yml + - contrib/nix/** + - pyproject.toml + - rust-toolchain.toml + - uv.lock pull_request: paths: *paths diff --git a/.github/workflows/pkg_params.yml b/.github/workflows/pkg_params.yml index 47f5bd0c..9f9c5f6e 100644 --- a/.github/workflows/pkg_params.yml +++ b/.github/workflows/pkg_params.yml @@ -13,6 +13,10 @@ on: - .github/workflows/pkg_params.yml - .github/workflows/build_stable.yml - .github/workflows/build_nightly.yml + - contrib/nix/** + - pyproject.toml + - rust-toolchain.toml + - uv.lock pull_request: paths: *paths diff --git a/.github/workflows/pkg_pkc.yml b/.github/workflows/pkg_pkc.yml index da71da1d..7d0cbb25 100644 --- a/.github/workflows/pkg_pkc.yml +++ b/.github/workflows/pkg_pkc.yml @@ -12,6 +12,10 @@ on: - .github/workflows/pkg_pkc.yml - .github/workflows/build_stable.yml - .github/workflows/build_nightly.yml + - contrib/nix/** + - pyproject.toml + - rust-toolchain.toml + - uv.lock pull_request: paths: *paths diff --git a/.github/workflows/pkg_pow.yml b/.github/workflows/pkg_pow.yml index 5a95334c..26b1816d 100644 --- a/.github/workflows/pkg_pow.yml +++ b/.github/workflows/pkg_pow.yml @@ -11,6 +11,10 @@ on: - .github/workflows/pkg_pow.yml - .github/workflows/build_stable.yml - .github/workflows/build_nightly.yml + - contrib/nix/** + - pyproject.toml + - rust-toolchain.toml + - uv.lock pull_request: paths: *paths diff --git a/.github/workflows/pkg_primitives.yml b/.github/workflows/pkg_primitives.yml index 56ecdb49..40fcaf99 100644 --- a/.github/workflows/pkg_primitives.yml +++ b/.github/workflows/pkg_primitives.yml @@ -13,6 +13,10 @@ on: - .github/workflows/pkg_primitives.yml - .github/workflows/build_stable.yml - .github/workflows/build_nightly.yml + - contrib/nix/** + - pyproject.toml + - rust-toolchain.toml + - uv.lock pull_request: paths: *paths diff --git a/.github/workflows/pkg_script.yml b/.github/workflows/pkg_script.yml index 7b40c41e..f49edae5 100644 --- a/.github/workflows/pkg_script.yml +++ b/.github/workflows/pkg_script.yml @@ -11,6 +11,10 @@ on: - .github/workflows/pkg_script.yml - .github/workflows/build_stable.yml - .github/workflows/build_nightly.yml + - contrib/nix/** + - pyproject.toml + - rust-toolchain.toml + - uv.lock pull_request: paths: *paths diff --git a/.github/workflows/pkg_types.yml b/.github/workflows/pkg_types.yml index 3b6f8cdb..e6d4fff1 100644 --- a/.github/workflows/pkg_types.yml +++ b/.github/workflows/pkg_types.yml @@ -10,6 +10,10 @@ on: - .github/workflows/pkg_types.yml - .github/workflows/build_stable.yml - .github/workflows/build_nightly.yml + - contrib/nix/** + - pyproject.toml + - rust-toolchain.toml + - uv.lock pull_request: paths: *paths diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 5edfba6d..91a5e507 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -7,5 +7,6 @@ "ms-vscode-remote.remote-containers", "rust-lang.rust-analyzer", "tamasfe.even-better-toml", + "jnoortheen.nix-ide", ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index c3cec8ca..5f7eace3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,8 @@ { + "[nix]": { + "editor.defaultFormatter": "jnoortheen.nix-ide", + "editor.rulers": [80, 100] + }, "[rust]": { "editor.defaultFormatter": "rust-lang.rust-analyzer", "editor.rulers": [80, 120], diff --git a/contrib/docker/Dockerfile b/contrib/docker/Dockerfile new file mode 100644 index 00000000..147e5573 --- /dev/null +++ b/contrib/docker/Dockerfile @@ -0,0 +1,24 @@ +FROM nixos/nix:2.35.2 + +SHELL ["/bin/sh", "-c"] + +ENV PATH="/usr/local/bin:$PATH" + +RUN rm /etc/nix/nix.conf + +COPY ./nix.conf /etc/nix/nix.conf +COPY ./daemon /usr/local/bin/daemon +COPY ./entrypoint /usr/local/bin/entrypoint + +RUN chmod +x /usr/local/bin/daemon /usr/local/bin/entrypoint + +ARG HOST_UID=1000 +ARG HOST_GID=1000 + +RUN printf 'nixuser:x:%s:\n' "${HOST_GID}" >> /etc/group \ + && printf 'nixuser:x:%s:%s:Nix user:/var/cache/base-sdk/home:/bin/sh\n' \ + "${HOST_UID}" "${HOST_GID}" >> /etc/passwd + +WORKDIR "/src/base-sdk" + +ENTRYPOINT ["/usr/local/bin/entrypoint"] diff --git a/contrib/docker/README.md b/contrib/docker/README.md new file mode 100644 index 00000000..3706874f --- /dev/null +++ b/contrib/docker/README.md @@ -0,0 +1,61 @@ +## Docker + +> [!TIP] +> On some platforms, `docker compose` is not included with the baseline Docker installation. In that case, you may +> need to consult platform-specific guidance on installing Compose, like the +> [`docker-compose`](https://packages.debian.org/trixie/docker-compose) package on Debian. + +To install Docker on your host, see [official guidance](https://docs.docker.com/get-started/get-docker/) for your +platform. Note that unlike using Nix, the store used in Docker _cannot_ be shared with the host and using the provided +containers is highly discouraged if you already use Nix on your host. + +**The Nix store is persisted as the volume `nix_store` and is expected to consume 15-20GB at a minimum, it is managed +by the `nix_daemon` container and other Nix daemons must not compete for management of this store.** + +> [!WARNING] +> The workspace is bind-mounted and the Compose project is incompatible with worktrees. If you are using paired +> programming assistants like Claude Code or Codex, there is a fair chance worktrees are in use. Worktrees resolve +> their parent repository by absolute path on the host, which isn't visible from the vantage point of the container. + +To build the image, from the [`contrib/docker`](.) directory, run + +```bash +docker compose build +``` + +### Entering a shell + +> [!NOTE] +> To prevent permissions issues, `HOST_UID` and `HOST_GID` are supplied to ensure that the container uses the same +> UID:GID pair as the source code it is bind-mounted against. If undefined, they default to the default Linux pair, +> `1000:1000`. + +To enter an interactive shell, from the [`contrib/docker`](.) directory, run + +```bash +# Starts the containers and drops you into an interactive shell, reaped on exit. The daemon is left running +HOST_UID=$(id -u) HOST_GID=$(id -g) docker compose run --rm nix_shell +``` + +To shut down the daemon, from the [`contrib/docker`](.) directory, run + +```bash +docker compose down +``` + +### One-shot commands + +To execute a command _without_ switching to a shell; or for scripting, from the [`contrib/docker`](.) directory, run + +```bash +docker compose run --rm nix_shell cargo build --workspace +``` + +### Reaping the Nix store + +To get rid of the persistent store and the build cache, freeing the associated space, from the +[`contrib/docker`](.) directory, run + +```bash +docker compose down --volumes +``` diff --git a/contrib/docker/daemon b/contrib/docker/daemon new file mode 100755 index 00000000..e4f96093 --- /dev/null +++ b/contrib/docker/daemon @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -eo pipefail + +install -d -m 0755 -o "${HOST_UID}" -g "${HOST_GID}" \ + /var/cache/base-sdk \ + /var/cache/base-sdk/home + +exec nix-daemon diff --git a/contrib/docker/docker-compose.yml b/contrib/docker/docker-compose.yml new file mode 100644 index 00000000..50bd9b97 --- /dev/null +++ b/contrib/docker/docker-compose.yml @@ -0,0 +1,56 @@ +name: "base-sdk" + +services: + nix_daemon: + build: + context: "." + dockerfile: "./Dockerfile" + args: + HOST_UID: "${HOST_UID:-1000}" + HOST_GID: "${HOST_GID:-1000}" + entrypoint: ["/usr/local/bin/daemon"] + environment: + HOST_UID: "${HOST_UID:-1000}" + HOST_GID: "${HOST_GID:-1000}" + init: true + healthcheck: + test: ["CMD-SHELL", "test -S /nix/var/nix/daemon-socket/socket"] + interval: "1s" + timeout: "2s" + retries: 10 + restart: "unless-stopped" + volumes: + - "build_cache:/var/cache/base-sdk" + - "nix_store:/nix" + + nix_shell: + build: + context: "." + dockerfile: "./Dockerfile" + args: + HOST_UID: "${HOST_UID:-1000}" + HOST_GID: "${HOST_GID:-1000}" + depends_on: + nix_daemon: + condition: "service_healthy" + init: true + user: "${HOST_UID:-1000}:${HOST_GID:-1000}" + environment: + NIX_REMOTE: "daemon" + HOME: "/var/cache/base-sdk/home" + USER: "nixuser" + WORKSPACE_PATH: "/src/base-sdk" + stdin_open: true # Equivalent to -i + tty: true # Equivalent to -t + volumes: + - "build_cache:/var/cache/base-sdk" + - "nix_store:/nix" + - type: bind + source: "../.." + target: "/src/base-sdk" + bind: + create_host_path: false + +volumes: + nix_store: + build_cache: diff --git a/contrib/docker/entrypoint b/contrib/docker/entrypoint new file mode 100755 index 00000000..bba2dbcd --- /dev/null +++ b/contrib/docker/entrypoint @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +set -eo pipefail + +SOCKET="/nix/var/nix/daemon-socket/socket" +if [[ ! -S "${SOCKET}" ]]; then + echo "${0##*/}: no nix daemon at ${SOCKET}, cannot continue!" >&2 + exit 1 +fi + +: "${WORKSPACE_PATH:?no workspace to enter, expected a bind mount}" +if ! git -C / config --global --fixed-value --get safe.directory "${WORKSPACE_PATH}" > /dev/null; then + git -C / config --global --add safe.directory "${WORKSPACE_PATH}" +fi + +cd "${WORKSPACE_PATH}" + +if ! git rev-parse --git-dir > /dev/null 2>&1; then + if [[ -f .git ]]; then + gitdir="$(< .git)" + echo "${0##*/}: ${WORKSPACE_PATH} is a worktree of ${gitdir#gitdir: }, unsupported!" >&2 + else + echo "${0##*/}: ${WORKSPACE_PATH} is not a git repository, cannot continue!" >&2 + fi + exit 1 +fi + +if (( $# > 0 )); then + exec nix develop ./contrib/nix#dev --command "$@" +else + exec nix develop ./contrib/nix#dev +fi diff --git a/contrib/docker/nix.conf b/contrib/docker/nix.conf new file mode 100644 index 00000000..9bc229a8 --- /dev/null +++ b/contrib/docker/nix.conf @@ -0,0 +1,8 @@ +always-allow-substitutes = true +bash-prompt-prefix = (nix:$name)\040 +build-users-group = nixbld +experimental-features = nix-command flakes +sandbox = false + +substituters = https://cache.nixos.org/ https://dashpay-base-sdk.cachix.org https://mirrors.tuna.tsinghua.edu.cn/nix-channels/store +trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= dashpay-base-sdk.cachix.org-1:TAoDyYL2e60lUEG8n10gvIZmujM5rJAhaG613fNfOWI= diff --git a/contrib/nix/README.md b/contrib/nix/README.md new file mode 100644 index 00000000..e7ee6f24 --- /dev/null +++ b/contrib/nix/README.md @@ -0,0 +1,48 @@ + + +## Nix + +To maintain a consistent development environment and reproducible toolchain, a declarative environment is available +using [Nix](https://nixos.org) on macOS and Linux hosts on ARM64 and AMD64. **Windows users are recommended to either +resort to using Nix through [Windows Subsystem for Linux](https://github.com/microsoft/WSL) or manually set up their +environment.** + + + + + +### Setting up Nix + +> [!WARNING] +> +> macOS 26 "Tahoe" is the last release supporting Intel-based Macs +> ([source](https://developer.apple.com/videos/play/wwdc2025/102/?time=3296)). Support for it as a _host_ is on a +> best-effort basis while it is still an officially supported _target_ platform. `nixpkgs` dropped `x86_64-darwin` +> per [NixOS/nixpkgs#535508](https://github.com/NixOS/nixpkgs/pull/535508) (included in 26.11). +> +> The environment is therefore pinned to its prior release, 26.05, and will stay there for as long as it remains +> reasonable. + +For install guidance on Linux, see [here](https://nixos.org/download/#nix-install-linux). For macOS, while Nix is an +option, Determinate Nix has been found to better accommodate macOS-specific quirks and guidance for that is available +[here](https://docs.determinate.systems/determinate-nix/#getting-started). That being said, regardless of choice of Nix +distribution used (including independent projects like [Lix](https://lix.systems/install/)), `nix-command` and `flakes` +features need to be enabled (guidance for enablement should be taken from your distribution vendor). + +### Entering a shell + +To enter an interactive shell, from the repository root, use + +```bash +nix develop ./contrib/nix#dev +``` + +### One-shot commands + +To execute a command _without_ switching to a shell; or for scripting, use + +```bash +nix develop ./contrib/nix#dev --command cargo test --workspace --features full +``` + + diff --git a/contrib/nix/flake.lock b/contrib/nix/flake.lock new file mode 100644 index 00000000..3c880c22 --- /dev/null +++ b/contrib/nix/flake.lock @@ -0,0 +1,120 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1787962033, + "narHash": "sha256-u6z9VTZA4Kf3RkHQo9sQI7NI4Ei/uiU9vrMqOiwWP1Y=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c5c4a43b0e8056328ec4529f735cabdb8f1942bb", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-26.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "pyproject-build-systems": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "pyproject-nix": [ + "pyproject-nix" + ], + "uv2nix": [ + "uv2nix" + ] + }, + "locked": { + "lastModified": 1788149501, + "narHash": "sha256-XKJP4KvhawV3pyVfraX3sflz05Rp7KP74gRt6QqKjNE=", + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "rev": "150839ac67b5a34db56a55e8f6b7099a4e7878ab", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "type": "github" + } + }, + "pyproject-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1786031528, + "narHash": "sha256-cROiHKO3UbIKqF5FG5NikvydzlfIj4EcR1Cty9qOVt4=", + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "rev": "1b1485546d85f6f6c7aadb10c4923dbc09633263", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "pyproject-build-systems": "pyproject-build-systems", + "pyproject-nix": "pyproject-nix", + "rust-overlay": "rust-overlay", + "uv2nix": "uv2nix" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1788077403, + "narHash": "sha256-c20YZQKzQ27qC6Wh729fWRTpI7oV4+7Ur4icKssD4Wc=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "89e26eeaafa88a2ede4778734acc794ff0299beb", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "uv2nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "pyproject-nix": [ + "pyproject-nix" + ] + }, + "locked": { + "lastModified": 1788001239, + "narHash": "sha256-AELmsXPI546MhbC/ZXC7WRUkCz7d4rqKTHUmliIgPpI=", + "owner": "pyproject-nix", + "repo": "uv2nix", + "rev": "7f9c6b613d2e749e54854b1d60ab6a2192db889e", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "uv2nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/contrib/nix/flake.nix b/contrib/nix/flake.nix new file mode 100644 index 00000000..ff9e5430 --- /dev/null +++ b/contrib/nix/flake.nix @@ -0,0 +1,85 @@ +# Development shells for the Dash Base SDK + +{ + description = "Development shells for the Dash Base SDK"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; + + pyproject-build-systems = { + url = "github:pyproject-nix/build-system-pkgs"; + inputs.nixpkgs.follows = "nixpkgs"; + inputs.pyproject-nix.follows = "pyproject-nix"; + inputs.uv2nix.follows = "uv2nix"; + }; + + pyproject-nix = { + url = "github:pyproject-nix/pyproject.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + uv2nix = { + url = "github:pyproject-nix/uv2nix"; + inputs.nixpkgs.follows = "nixpkgs"; + inputs.pyproject-nix.follows = "pyproject-nix"; + }; + }; + + outputs = + { + nixpkgs, + rust-overlay, + ... + }@inputs: + let + inherit (nixpkgs) lib; + + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + + eachSystem = + f: + lib.genAttrs systems ( + system: + f ( + import nixpkgs { + inherit system; + config.allowUnfreePredicate = + pkg: + builtins.elem (lib.getName pkg) [ + "codeql" + "xcode-sdk" + ]; + overlays = [ rust-overlay.overlays.default ]; + } + ) + ); + in + { + devShells = eachSystem ( + pkgs: + let + ctx = import ./shell/common.nix { + inherit pkgs lib inputs; + root = ../..; + }; + ci = import ./shell/ci.nix ctx; + in + { + inherit ci; + dev = import ./shell/dev.nix (ctx // { inherit ci; }); + } + ); + + formatter = eachSystem (pkgs: pkgs.nixfmt); + }; +} diff --git a/contrib/nix/mods/codeql.nix b/contrib/nix/mods/codeql.nix new file mode 100644 index 00000000..41502225 --- /dev/null +++ b/contrib/nix/mods/codeql.nix @@ -0,0 +1,102 @@ +# CodeQL CLI + +{ pkgs, lib }: + +let + version = "2.26.1"; + linux64 = { + file = "codeql-linux64.zip"; + hash = "sha256-FUgN2m4gM2qcfdy2Fx4OkVXx/nOh0xuurBU4IcuJrqs="; + }; + osx64 = { + file = "codeql-osx64.zip"; + hash = "sha256-YcXStT4c2O4r1XwxpVxXr1P/qv3xnEbSNBcExsrPNdM="; + }; + + # CodeQL does not offer ARM64 builds for Linux (github/codeql#20616), so we + # resort to x86_64 emulation instead. macOS releases include both AMD64 and + # ARM64 support, mitigating the need for emulation. + targets = { + x86_64-linux = { + asset = linux64; + dir = "linux64"; + emulate = false; + }; + aarch64-linux = { + asset = linux64; + dir = "linux64"; + emulate = true; + }; + x86_64-darwin = { + asset = osx64; + dir = "osx64"; + emulate = false; + }; + aarch64-darwin = { + asset = osx64; + dir = "osx64"; + emulate = false; + }; + }; + + target = targets.${pkgs.stdenv.hostPlatform.system}; + + # The Linux archive names its tracer lib64trace.so and bundles an x86_64 JDK + # that will not run from a Nix store. + linuxFixup = '' + ln -sf $out/codeql/tools/linux64/lib64trace.so $out/codeql/tools/linux64/libtrace.so + rm -rf $out/codeql/tools/linux64/java + ln -s ${pkgs.zulu17} $out/codeql/tools/linux64/java + ''; + + # Wrapping all executables around QEMU to achieve x86_64 emulation. Shared + # objects are unmodified to avoid caller dlopen() breakage. + emulateFixup = '' + find $out/codeql -type f -perm -u+x -print0 | + while IFS= read -r -d "" bin; do + case "$(file -b "$bin")" in + *ELF*executable*x86-64*) + mv "$bin" "$bin.x86_64" + cat > "$bin" < + # first and errors out if it cannot. + libCxx = _: [ "-iwithsysroot/usr/include/c++/v1" ]; + + cxxExtra = + d: + { + darwin = libCxx; + glibc = libStdCxx; + mingw = mingwLibStdCxx; + } + .${d.kind} + d; + + flagsFor = + d: + { + mingw = mingwFlags; + glibc = glibcFlags; + darwin = darwinFlags; + } + .${d.kind} + d; + + # rustc shells out to -dlltool for the raw-dylib imports windows-sys + # declares, and looks for that exact name, not llvm-dlltool. Every binary in + # this package is target-prefixed, so none of it shadows a host tool. + extraPkgs = d: lib.optionals (d.kind == "mingw") [ d.cross.stdenv.cc.bintools.bintools ]; + + # A driver per target and language. C_INCLUDE_PATH and CPLUS_INCLUDE_PATH + # are unset because the host's include paths would otherwise leak into a + # cross compile. + driver = + name: bin: d: extra: + pkgs.writeShellScriptBin name '' + exec env -u C_INCLUDE_PATH -u CPLUS_INCLUDE_PATH \ + ${llvm.clang-unwrapped}/bin/${bin} \ + --target=${d.clangTarget} \ + -resource-dir=${resourceDir} \ + ${lib.concatStringsSep " \\\n " (extra ++ flagsFor d)} \ + "$@" + ''; + + # `cc-rs` and `cargo` spell the same target differently. + ccKey = t: builtins.replaceStrings [ "-" ] [ "_" ] t; + cargoKey = t: lib.toUpper (ccKey t); + + defFor = t: defs.${t} or (throw "cxx.nix knows no C toolchain for ${t}"); + + wire = + t: + let + d = defFor t; + cc = driver "${t}-cc" "clang" d [ ]; + cxx = driver "${t}-c++" "clang++" d (cxxExtra d); + in + { + packages = [ + cc + cxx + ] + ++ extraPkgs d; + env = { + "CC_${ccKey t}" = "${cc}/bin/${t}-cc"; + "CXX_${ccKey t}" = "${cxx}/bin/${t}-c++"; + "AR_${ccKey t}" = "${llvm.bintools-unwrapped}/bin/llvm-ar"; + "CARGO_TARGET_${cargoKey t}_LINKER" = "${cc}/bin/${t}-cc"; + }; + }; +in +{ + # Adding clang to packages would not displace the cc-wrapper the default + # stdenv puts on PATH, so the shell is built against this stdenv instead. + compiler = { + stdenv = llvm.stdenv; + packages = [ llvm.bintools ]; + }; + + # attrNames does not force the values, so listing targets is cheap even + # though each entry reaches for a pkgsCross set. + knownTargets = builtins.attrNames defs; + + forTargets = + targets: + let + wired = map wire targets; + + # `rustc` asks `xcrun` for the SDK and picks its own deployment target, so + # both are set shell-wide rather than per target. A native macOS build + # has to resolve against the same pinned SDK as a cross target. + darwin = lib.optionalAttrs (lib.any (t: (defFor t).kind == "darwin") targets) { + MACOSX_DEPLOYMENT_TARGET = xcodeSdk.minVersion; + SDKROOT = "${xcodeSdk}"; + }; + in + { + packages = lib.concatMap (w: w.packages) wired; + env = lib.foldl' (a: w: a // w.env) darwin wired; + }; +} diff --git a/contrib/nix/mods/nixpkgs.nix b/contrib/nix/mods/nixpkgs.nix new file mode 100644 index 00000000..da9fd1f9 --- /dev/null +++ b/contrib/nix/mods/nixpkgs.nix @@ -0,0 +1,19 @@ +# Native packages sourced from the Nix Packages collection (nixpkgs) + +{ pkgs }: + +{ + packages = [ + pkgs.cargo-llvm-cov + pkgs.git + pkgs.nixfmt + pkgs.nodejs_24 + pkgs.wasm-pack + + # Packages synced with '.tools' from 'pyproject.toml'. + pkgs.ruff + pkgs.semgrep + pkgs.taplo + pkgs.zensical + ]; +} diff --git a/contrib/nix/mods/python.nix b/contrib/nix/mods/python.nix new file mode 100644 index 00000000..5bd5ae25 --- /dev/null +++ b/contrib/nix/mods/python.nix @@ -0,0 +1,70 @@ +# Packages sourced from PyPI against lockfile imported with uv2nix + +{ + pkgs, + lib, + uv2nix, + pyproject-nix, + pyproject-build-systems, + workspaceRoot, + python, +}: + +let + workspace = uv2nix.lib.workspace.loadWorkspace { inherit workspaceRoot; }; + + # Prefer binary distributions (i.e. wheels) when we can, saves us some build + # complexity. + overlay = workspace.mkPyprojectOverlay { sourcePreference = "wheel"; }; + + overrides = final: prev: { + # rjsmin doesn't publish wheels for macOS. uv.lock does not track build + # dependencies (see astral-sh/uv#5190), so we manually define 'setuptools'. + rjsmin = prev.rjsmin.overrideAttrs (old: { + nativeBuildInputs = + (old.nativeBuildInputs or [ ]) ++ final.resolveBuildSystem { setuptools = [ ]; }; + }); + + # Narrow rebuild triggers to relevant files to avoid pulling in the whole + # source tree and thrashing the cache for it. + dash-base-sdk = prev.dash-base-sdk.overrideAttrs (_: { + src = lib.fileset.toSource { + root = workspaceRoot; + fileset = lib.fileset.unions [ + (workspaceRoot + "/pyproject.toml") + (workspaceRoot + "/uv.lock") + ]; + }; + }); + }; + + pythonSet = (pkgs.callPackage pyproject-nix.build.packages { inherit python; }).overrideScope ( + lib.composeManyExtensions [ + pyproject-build-systems.overlays.default + overlay + overrides + ] + ); + + # '.dev' is a union of '.lib' and '.tools', '.tools' is sourced from nixpkgs. + # Sourcing '.lib' satisfies '.dev', completing the dependency list. + venv = pythonSet.mkVirtualEnv "dash-base-sdk-lib" { dash-base-sdk = [ "lib" ]; }; +in +{ + packages = [ + venv + pkgs.uv + ]; + + env = { + UV_PYTHON = "${venv}/bin/python"; + UV_NO_SYNC = "1"; + }; + + # `semgrep` and other Python applications place their own interpreter + # in PATH, eclipsing our interpreter, preventing it from importing our + # workspaces packages. + shellHook = '' + export PATH="${venv}/bin:$PATH" + ''; +} diff --git a/contrib/nix/mods/rust.nix b/contrib/nix/mods/rust.nix new file mode 100644 index 00000000..32593b25 --- /dev/null +++ b/contrib/nix/mods/rust.nix @@ -0,0 +1,22 @@ +# Rust toolchains, default included in PATH, remaining as TOOLCHAIN_* + +{ + default, + lib, + toolchains, +}: + +let + # A path per non-default toolchain, since only one can own `cargo` at a time. + named = lib.mapAttrs' (name: t: lib.nameValuePair "TOOLCHAIN_${lib.toUpper name}" "${t}") ( + lib.filterAttrs (name: _: name != default) toolchains + ); +in +{ + packages = [ toolchains.${default} ]; + + env = { + CARGO_TERM_COLOR = "always"; + } + // named; +} diff --git a/contrib/nix/mods/xcode_sdk.nix b/contrib/nix/mods/xcode_sdk.nix new file mode 100644 index 00000000..695e6427 --- /dev/null +++ b/contrib/nix/mods/xcode_sdk.nix @@ -0,0 +1,42 @@ +# Xcode-derived macOS SDK with libcxx headers + +{ pkgs }: + +let + # Xcode release and build ID + version = "26.1.1-17B100"; +in + +pkgs.stdenvNoCC.mkDerivation { + inherit version; + + # Archive contains stubs and headers, nothing to configure, build or fix + dontBuild = true; + dontConfigure = true; + dontFixup = true; + + pname = "xcode-sdk"; + src = pkgs.fetchurl { + url = "https://bitcoincore.org/depends-sources/sdks/Xcode-${version}-extracted-SDK-with-libcxx-headers.tar"; + hash = "sha256-lgD6k2RN9nTukWteLIprqNrPYxmWpl3JItADuYteo7E="; + }; + + installPhase = '' + runHook preInstall + mkdir -p $out + cp -a ./* $out/ + runHook postInstall + ''; + + passthru = { + # Target macOS version binaries are expected to support. + minVersion = "14.0"; + # ld64 version clang is told to assume, as lld reports its own. + linkerVersion = "711"; + }; + + meta = { + description = "macOS SDK extracted from Xcode, with libc++ headers"; + license = pkgs.lib.licenses.unfree; + }; +} diff --git a/contrib/nix/shell/ci.nix b/contrib/nix/shell/ci.nix new file mode 100644 index 00000000..28d704ff --- /dev/null +++ b/contrib/nix/shell/ci.nix @@ -0,0 +1,18 @@ +# Development shell for continuous integration + +{ + compose, + crossTargets, + cxx, + mods, + ... +}: + +compose [ + (cxx.forTargets crossTargets) + mods.codeql + mods.cxx + mods.nixpkgs + mods.python + mods.rust +] diff --git a/contrib/nix/shell/common.nix b/contrib/nix/shell/common.nix new file mode 100644 index 00000000..434b8f7b --- /dev/null +++ b/contrib/nix/shell/common.nix @@ -0,0 +1,109 @@ +# Common logic shared between development shells + +{ + pkgs, + lib, + inputs, + root, +}: + +let + # Target platform for web demos bundled with documentation. + commonTargets = [ "wasm32-unknown-unknown" ]; + + cxx = import ../mods/cxx.nix { + inherit pkgs lib; + xcodeSdk = import ../mods/xcode_sdk.nix { inherit pkgs; }; + }; + + rsComponents = (lib.importTOML (root + "/rust-toolchain.toml")).toolchain.components; + + hostTriple = pkgs.stdenv.hostPlatform.rust.rustcTarget; + sameOs = t: lib.hasInfix (if pkgs.stdenv.hostPlatform.isDarwin then "apple-darwin" else "linux") t; + crossTargets = lib.filter (t: t != hostTriple && sameOs t) cxx.knownTargets; + + # Everything else the table knows, which needs a sysroot of its own and so + # only the dev shell carries. + foreignTargets = lib.filter (t: t != hostTriple && !sameOs t) cxx.knownTargets; + + # rust-overlay propagates a C compiler wrapper with every toolchain, which + # would take precedence over `cxx.nix`'s definitions. We strip it here so that + # every shell reaching for a toolchain respects our definitions. + bare = + toolchain: + toolchain.overrideAttrs (_: { + propagatedBuildInputs = [ ]; + depsHostHostPropagated = [ ]; + depsTargetTargetPropagated = [ ]; + }); + + nightlyWith = + extra: + bare ( + (pkgs.rust-bin.fromRustupToolchainFile (root + "/rust-toolchain.toml")).override { + targets = commonTargets ++ extra; + } + ); + + # Folds modules into mkShell arguments. Conflicting variables or stdenvs + # will throw instead of allowing order-sensitive assignment. + compose = + mods: + let + named = m: m._name or ""; + envs = map (m: m.env or { }) mods; + names = lib.concatMap lib.attrNames envs; + clashes = lib.unique (lib.filter (n: lib.count (m: m == n) names > 1) names); + chosen = lib.filter (m: (m.stdenv or null) != null) mods; + mkShell = + if chosen == [ ] then + pkgs.mkShell + else if lib.length chosen == 1 then + pkgs.mkShell.override { stdenv = (lib.head chosen).stdenv; } + else + throw "stdenv redefined: ${lib.concatMapStringsSep ", " named chosen}"; + in + if clashes != [ ] then + throw "variables redefined: ${lib.concatStringsSep ", " clashes}" + else + mkShell ( + { + packages = lib.concatMap (m: m.packages or [ ]) mods; + shellHook = lib.concatStringsSep "\n" (lib.filter (h: h != "") (map (m: m.shellHook or "") mods)); + } + // lib.foldl' (a: b: a // b) { } envs + ); +in +{ + inherit + pkgs + lib + compose + crossTargets + cxx + foreignTargets + nightlyWith + ; + + mods = lib.mapAttrs (name: m: m // { _name = name; }) { + codeql = import ../mods/codeql.nix { inherit pkgs lib; }; + cxx = cxx.compiler; + nixpkgs = import ../mods/nixpkgs.nix { inherit pkgs; }; + python = import ../mods/python.nix { + inherit pkgs lib; + inherit (inputs) uv2nix pyproject-nix pyproject-build-systems; + workspaceRoot = root; + # Must match `project.requires-python` in pyproject.toml, effective floor. + python = pkgs.python311; + }; + rust = import ../mods/rust.nix { + inherit lib; + default = "nightly"; + toolchains = { + nightly = nightlyWith crossTargets; + # Must match `workspace.package.rust-version` in root Cargo.toml. + msrv = bare (pkgs.rust-bin.stable."1.85.0".minimal.override { extensions = rsComponents; }); + }; + }; + }; +} diff --git a/contrib/nix/shell/dev.nix b/contrib/nix/shell/dev.nix new file mode 100644 index 00000000..2a6d3ae9 --- /dev/null +++ b/contrib/nix/shell/dev.nix @@ -0,0 +1,81 @@ +# Development shell for interactive instances + +{ + ci, + cxx, + crossTargets, + foreignTargets, + lib, + nightlyWith, + pkgs, + ... +}: + +let + cross = cxx.forTargets foreignTargets; + toolchain = nightlyWith (crossTargets ++ foreignTargets); + + ohMyBash = pkgs.fetchFromGitHub { + owner = "ohmybash"; + repo = "oh-my-bash"; + rev = "abf846186ab0a8a41ec5888e827ece6277dfe446"; + hash = "sha256-tlYKhz7baZ02FcHlYMnLQsgjEsN/K+z3+UWre4mS5Qs="; + }; + + tmuxConf = pkgs.writeText "tmux.conf" '' + set -g default-terminal "tmux-256color" + set -ga terminal-overrides ",xterm-256color:Tc" + set -g alternate-screen off + set -g base-index 1 + set -g detach-on-destroy on + set -g history-limit 20000 + set -g mouse on + set -g pane-base-index 1 + set -g renumber-windows on + ''; + + # tmux reads a file named on its command line or one under $HOME, and a + # devshell owns neither, so the flag is bound to the binary instead. + tmux = pkgs.writeShellScriptBin "tmux" '' + exec ${pkgs.tmux}/bin/tmux -f ${tmuxConf} "$@" + ''; + + utilities = [ + pkgs.bashInteractive + pkgs.curl + pkgs.dnsutils + pkgs.fd + pkgs.gh + pkgs.gnupg + pkgs.jq + pkgs.nano + pkgs.pv + pkgs.ripgrep + pkgs.rsync + pkgs.unzip + pkgs.wget + tmux + ] + # Darwin carries `ping` in its base system, Linux does not. + ++ lib.optionals pkgs.stdenv.hostPlatform.isLinux [ pkgs.iputils ]; +in + +ci.overrideAttrs ( + old: + { + # mkShell places `packages` here. + nativeBuildInputs = old.nativeBuildInputs ++ cross.packages ++ utilities ++ [ toolchain ]; + + # oh-my-bash is meant for interactive use. `--command` has no use for it. + shellHook = (old.shellHook or "") + '' + export PATH="${toolchain}/bin:$PATH" + + export OSH="${ohMyBash}" + if [[ $- == *i* ]]; then + OSH_THEME="rr" + source "$OSH/oh-my-bash.sh" + fi + ''; + } + // cross.env +) diff --git a/contrib/nix/smoke_test.sh b/contrib/nix/smoke_test.sh new file mode 100755 index 00000000..6c3ec9f5 --- /dev/null +++ b/contrib/nix/smoke_test.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash + +set -uo pipefail + +out="${1:-$(mktemp -d)}" +mkdir -p "${out}" +printf '#include \nint main(void) { puts("Hello, world!"); return 0; }\n' > "${out}/hello.c" +printf '#include \nint main() { std::cout << "Hello, world!\\n"; }\n' > "${out}/hello.cpp" +printf 'fn main() { println!("Hello, world!"); }\n' > "${out}/hello.rs" + +status=0 + +# `--macho` is a `llvm-objdump` flag, the GNU equivalent is unsupported. +objdump=$(command -v llvm-objdump || command -v objdump || true) + +say() { + printf ' %-26s %-4s %s\n' "$1" "$2" "$3" +} + +produced() { + local found=("${1}"*) + [[ -e "${found[0]}" ]] +} + +try() { + local name="$1" lang="$2" bin="${out}/${1}.${2}" want="${MACOSX_DEPLOYMENT_TARGET:-}" got kind + shift 2 + if [[ -z "$1" ]]; then + say "${name}" "${lang}" skipped + return + fi + rm -f -- "${bin}"* + if ! "$@" "${out}/hello.${lang}" -o "${bin}" || ! produced "${bin}"; then + say "${name}" "${lang}" FAILED + status=1 + return + fi + # An unreadable load command is a failure, not a reason to skip the check. + if [[ "${name}" == *-apple-darwin ]]; then + got=$("${objdump:-false}" --macho --private-headers "${bin}"* 2> /dev/null | + awk '$1 == "minos" { print $2; exit }') + if [[ -z "${want}" || "${got}" != "${want}" ]]; then + say "${name}" "${lang}" "FAILED, minos ${got:-unknown}, wanted ${want:-}" + status=1 + return + fi + fi + kind=$(file -b "${bin}"* 2> /dev/null | head -1) + say "${name}" "${lang}" "${kind:-built}" +} + +try host c "${CC:-cc}" +try host cpp "${CXX:-c++}" +try host rs rustc + +for target in \ + aarch64-apple-darwin \ + x86_64-apple-darwin \ + aarch64-unknown-linux-gnu \ + x86_64-unknown-linux-gnu \ + x86_64-pc-windows-gnu \ + wasm32-unknown-unknown; +do + cc="CC_${target//-/_}" + cxx="CXX_${target//-/_}" + driver="${!cc:-}" + rust="${driver:+rustc}" + [[ "${target}" != wasm32-* ]] || rust=rustc + try "${target}" c "${driver}" + try "${target}" cpp "${!cxx:-}" + try "${target}" rs "${rust}" "--target=${target}" ${driver:+"-Clinker=${driver}"} +done + +exit "${status}" diff --git a/docs/build_docs.py b/docs/build_docs.py index bc9a54d9..70b97f85 100755 --- a/docs/build_docs.py +++ b/docs/build_docs.py @@ -108,8 +108,8 @@ def _build_wasm_samples(root: Path, wasm_pack: str, cfg: Config) -> None: "CARGO_TARGET_DIR": str(root / "target" / "samples"), "CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS": "-C target-feature=+simd128", - "RUSTUP_TOOLCHAIN": channel, } + env.setdefault("RUSTUP_TOOLCHAIN", channel) for cargo_toml in samples: crate_dir = cargo_toml.parent diff --git a/docs/dev/about_docs.md b/docs/dev/about_docs.md index 45514310..e50a7466 100644 --- a/docs/dev/about_docs.md +++ b/docs/dev/about_docs.md @@ -8,6 +8,7 @@ This guide is generated using [Zensical](https://pypi.org/project/zensical/) (a > [!NOTE] > If you haven't set up your development environment, check out the [startup guide](./getting_started.md) first. +> If you're in a [development shell](./devshells.md), `wasm-pack` is included and this step can be skipped. The documentation comes bundled with web-ready demos, which are powered by WebAssembly. Preparing them for distribution relies on [`wasm-pack`](https://github.com/wasm-bindgen/wasm-pack), which is installed as a binary crate. diff --git a/docs/dev/cross_compilation.md b/docs/dev/cross_compilation.md new file mode 100644 index 00000000..84ed10c9 --- /dev/null +++ b/docs/dev/cross_compilation.md @@ -0,0 +1,60 @@ +# Cross Compilation + +> [!NOTE] +> This guide assumes access to a [development shell](./devshells.md). Attempting cross-compilation outside a devshell +> is not within the scope of this guide and is unsupported. + +Cross-compilation is when the `host` and `target` of a binary diverge. Regular compilation has the goal of generating +immediately executable artifacts for the machine that is compiling (i.e. they share identical `host` and `target`), +while cross-compilation gains its value in being able to produce artifacts for consumption on other platforms without +procuring hardware or platform configurations for each desired target. + +To achieve cross-compilation, the following are supplied: + +* A thinned-down root filesystem (i.e. sysroot) with headers and libraries expected by `target` +* A compiler and linker that runs on the `host` but emits artifacts for the `target` +* The standard library required by `target` + +We use an LLVM 20 toolchain (Clang as the compiler, LLD as the linker) for Rust packages that bind C/C++ codebases +through an FFI, with the standard library bundle supplied to `rustc` by cargo. `rustc` itself is first class native +cross-compiler. + +> [!NOTE] +> Support for macOS and Windows cross-compilation is only available in the `#dev` devshell, it is omitted from the `#ci` +> devshell to reduce cache contention with our forge provider. + +The following platforms are supported as `target`s **excluding the `host` platform**. + +| Target | Object Format | Sysroot | +| --------------------------- | ------------- | --------------------------- | +| `aarch64-apple-darwin` | Mach-O | macOS SDK with libcxx | +| `x86_64-apple-darwin` | Mach-O | macOS SDK with libcxx | +| `aarch64-unknown-linux-gnu` | ELF | glibc | +| `x86_64-unknown-linux-gnu` | ELF | glibc | +| `x86_64-pc-windows-gnu` | PE32+ | MinGW-w64 | +| `wasm32-unknown-unknown` | Wasm | *None*, no libc(++) support | + +> [!NOTE] +> `` spells the triple with underscores, as `cc-rs` reads it (e.g. `CC_aarch64_apple_darwin`), while +> `` spells the triple in upper case, as `cargo` reads it (e.g. `CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER`). + +For each target, the following environment variables are defined + +| Environment Variable | Description | +| ------------------------------ | --------------------------------------- | +| `CC_` | The C compiler for `target` | +| `CXX_` | The C++ compiler for `target` | +| `AR_` | The archiver, `llvm-ar` | +| `CARGO_TARGET__LINKER` | The linker for `target` used by `cargo` | + +## Building (Rust) + +> [!TIP] +> `--all-targets` is recommended when performing smoke tests to ensure the linker behaves as expected, since it +> brings test binaries into scope. `base-sdk` is primarily a collection of library crates, so without binaries to +> link, configuration failures may not surface. + +```bash +# Building for ARM64 Linux (assuming an AMD64 host) +cargo build --workspace --all-targets --features full --target aarch64-unknown-linux-gnu +``` diff --git a/docs/dev/devshells.md b/docs/dev/devshells.md new file mode 100644 index 00000000..097d6297 --- /dev/null +++ b/docs/dev/devshells.md @@ -0,0 +1,56 @@ +# Development Shells + + + +> [!TIP] +> Should you wish to utilize devshells without installing Nix on your host, the environment is also available wrapped in +> a Docker [container](#docker); though it is still recommended to set up Nix for long-term development due to the cost +> associated with maintaining a parallel Nix store. + + + + + +### Quirks + + + +* **The Python environment is read-only** + + Development shells (devshells) are immutable, this extends to packages sourced from PyPI. `uv` will neither sync nor + install packages outside the initially defined set. To modify packages, edit [`pyproject.toml`](../../pyproject.toml), + then generate an updated lockfile by running `uv lock` in the shell. Then re-enter a fresh devshell, it should take on + the new definitions. + +* **Pinned versions of tools don't match against manual setup** + + Manual setup installs PyPI-sourced dependencies with `.dev`. Not every PyPI package is written in Python nor does it + have to expose a Pythonic API to qualify for publication on PyPI. This allows PyPI to serve as a general means to + distribute binaries so long as they otherwise meet PyPI's guidelines. + + This makes PyPI serve as a parallel package source and `.dev` leverages this to have better control over dependencies + instead of relying on platform-specific package sources. We do not do this in devshells, preferring `nixpkgs` when + feasible (and falling back on PyPI when it isn't, segmenting these packages as `.lib` in + [`pyproject.toml`](../../pyproject.toml)). This leads to predictable drift between the versions pinned in + [`uv.lock`](../../uv.lock) and versions published in the pinned snapshot of `nixpkgs` in + [`flake.lock`](../../contrib/nix/flake.lock). + + This drift is benign. Should there be any difference in outcome, consider + [filing an issue](https://github.com/dashpay/base-sdk/issues/new). + + + + + +### Quirks + + + +* **`nix_shell` doesn't work standalone** + + `nix_shell` intentionally does not host the daemon, instead, delegating that to a dedicated `nix_daemon` container. + This is to avoid churn and to achieve better isolation. Nix stores are expensive in storage cost (and initially for + built elements, compute), so the interactive container talks to the store-hosting container, permitting flexible + setups where multiple containers can leverage the same underlying store. + + Managing the containers _outside_ Docker Compose is unsupported. diff --git a/docs/dev/getting_started.md b/docs/dev/getting_started.md index ca260d6f..a4f79190 100644 --- a/docs/dev/getting_started.md +++ b/docs/dev/getting_started.md @@ -1,5 +1,10 @@ # Getting Started +> [!TIP] +> On ARM64 and AMD64 Linux and macOS hosts, we offer [development shells](./devshells.md) that provide environments +> identical to or based on environments used in CI as an alternative to manual setup. The following instructions are +> for manual setup. + ## Installing Rust > [!WARNING] diff --git a/docs/preprocess.py b/docs/preprocess.py index e2fccf70..4e50daf8 100644 --- a/docs/preprocess.py +++ b/docs/preprocess.py @@ -18,6 +18,11 @@ from typing import TYPE_CHECKING from common import off_disk, root_dir, spelt_as_stored +from markdown.blockprocessors import ( + ListIndentProcessor, + OListProcessor, + UListProcessor, +) from markdown.extensions import Extension from markdown.preprocessors import Preprocessor @@ -330,6 +335,22 @@ def _section(lines: list[str], name: str, spec: str) -> list[str]: return found +# Indentation where CommonMark defines a continued list item. +_LIST_INDENT = 2 + + +def _commonmark_list_indent(md: Markdown) -> None: + """Rebuild the list processors at CommonMark indentation.""" + kept = md.tab_length + md.tab_length = _LIST_INDENT + for processor in md.parser.blockprocessors: + if isinstance( + processor, ListIndentProcessor | OListProcessor | UListProcessor + ): + processor.__init__(md.parser) + md.tab_length = kept + + class PreprocessorHost(Extension): """Markdown extension entrypoint.""" @@ -348,6 +369,7 @@ def extendMarkdown(self, md: Markdown) -> None: ) md.preprocessors.register(include, "include", 32) md.preprocessors.register(GfmAlertsPreprocessor(md), "gfm_alerts", 31) + _commonmark_list_indent(md) def makeExtension(**kwargs: object) -> PreprocessorHost: @@ -382,6 +404,11 @@ def _scratch(**files: str) -> Iterator[Path]: (home / f"{stem}.md").write_text(text, encoding="utf-8") yield home.relative_to(root_dir()) + def test_list_continues_at_the_commonmark_column(self) -> None: + out = self._render("* lead\n\n continuation\n") + assert out.count("
  • ") == 1 + assert "continuation" in out.split("
  • ")[0] + def test_alert_becomes_admonition(self) -> None: out = self._render("> [!CAUTION]\n> Mind the gap.\n") assert 'class="admonition danger"' in out diff --git a/docs/zensical.toml b/docs/zensical.toml index 59f2d8a2..907fc245 100644 --- a/docs/zensical.toml +++ b/docs/zensical.toml @@ -18,6 +18,8 @@ nav = [ { "Home" = "README.md" }, { "Contributing" = [ { "Getting Started" = "dev/getting_started.md" }, + { "Cross Compilation" = "dev/cross_compilation.md" }, + { "Development Shells" = "dev/devshells.md" }, { "Documentation" = "dev/about_docs.md" }, { "Maintenance" = "dev/maintenance.md" }, { "Style Guide (Rust)" = "dev/guide_rust.md" }, diff --git a/maint/README.md b/maint/README.md index 6133d5f4..753b3818 100644 --- a/maint/README.md +++ b/maint/README.md @@ -18,6 +18,7 @@ use [`lint_all.py`](./lint_all.py). | [`lint_codeql.py`](./lint/lint_codeql.py) | Query Rust sources against [`maint/codeql/rust/*.ql`](./codeql/rust) | `check`, `apply`, `apply-all`, `run`, `run-all` | `codeql`, `rustc` | | [`lint_javascript.py`](./lint/lint_javascript.py) | Lint Javascript sources against [`eslint.config.mjs`](js/eslint.config.mjs) | *None* | `npx` (part of Node.js), `eslint` (auto-retrieved by script) | | [`lint_markdown.py`](./lint/lint_markdown.py) | Lint Markdown [documentation](../docs/dev/about_docs.md) | *None* | `pymarkdownlnt` | +| [`lint_nix.py`](./lint/lint_nix.py) | Lint Nix sources against `nixfmt`'s RFC 166 style | `check`, `apply`, `apply-all` | `nixfmt`, `git` | | [`lint_python.py`](./lint/lint_python.py) | Lint Python sources against `[tool.ruff]` options in [`pyproject.toml`](../pyproject.toml) | *None* | `ruff` | | [`lint_rust.py`](./lint/lint_rust.py) | Lint Rust sources against [`rustfmt.toml`](../rustfmt.toml) | *None* | `cargo`, `rustfmt` | | [`lint_semgrep.py`](./lint/lint_semgrep.py) | Lint source code against [`maint/semgrep`](./semgrep/rust) definitions | *None* | `semgrep` | diff --git a/maint/common.py b/maint/common.py index c45e2c2c..4b0e6d68 100644 --- a/maint/common.py +++ b/maint/common.py @@ -157,6 +157,69 @@ def git_out(cwd: Path | str, *args: str) -> str: return result.stdout.strip() +# Every formatter had grown this body independently, so the hint, the +# phrasing and the empty-list case were copies free to drift apart. +def formatted( + script: str, + noun: str, + sources: list[Path] | None, + command: Callable[[list[Path]], list[str]], + *, + fix: bool, + scoped: bool, + cwd: Path | None = None, + output: Callable[[str, str], None] | None = None, +) -> int: + """Hold *sources* to a formatter, or rewrite them, and say which happened. + + *command* is handed the paths and returns the argv to run. *output* is + handed what the tool wrote, as stdout and stderr, by a caller that has to + post-process it; left out, the tool writes to this process's own streams. + + *sources* is None where the tool finds its own files, which is the one + case a count cannot be reported for. + """ + if sources is not None and not sources: + print(f"{script}: no {noun} was touched") + return RETCODE_PASS + + result = subprocess.run( # noqa: S603 + command(sources or []), + capture_output=output is not None, + check=False, + cwd=None if cwd is None else str(cwd), + text=True, + ) + if output is not None: + output(result.stdout, result.stderr) + + if result.returncode != 0: + if not fix: + print( + f"hint: run 'python3 maint/lint/{script}.py apply-all' to rewrite", + file=sys.stderr, + ) + return RETCODE_ERR + + if scoped: + scope = f"{len(sources or [])} touched {noun}(s)" + elif sources is None: + scope = f"every {noun}" + else: + scope = f"every {noun} ({len(sources)})" + print(f"{script}: rewrote {scope}" if fix else f"{script}: {scope} conforms") + return RETCODE_PASS + + +def format_verbs(noun: str) -> dict[str, str]: + """Return the check/apply/apply-all verbs every formatter declares.""" + return { + "check": f"report every {noun} whose formatting differs", + "apply": f"rewrite the {noun} this branch changed vs {DEFAULT_BASE}", + "apply-all": f"rewrite every {noun} in the tree", + } + + def relay( text: str, repo_root: Path, diff --git a/maint/lint/lint_cargo.py b/maint/lint/lint_cargo.py index 0eb0082b..a27cf0ff 100755 --- a/maint/lint/lint_cargo.py +++ b/maint/lint/lint_cargo.py @@ -30,6 +30,7 @@ RETCODE_SKIP, declare_verbs, format_table, + formatted, relay, require_bin, root_dir, @@ -69,42 +70,33 @@ def _check_format( print(f"{e}, skipping the format check", file=sys.stderr) return None - if only is not None and not only: - print(f"{SCRIPT}: no TOML file was touched") - return RETCODE_PASS - - argv = [taplo, "fmt"] + ([] if fix else ["--check", "--diff"]) + (only or []) - result = subprocess.run( # noqa: S603 - argv, - capture_output=True, - check=False, - cwd=str(repo_root), - text=True, - ) - relay(result.stdout, repo_root) - - # Taplo reports the file count on stderr at INFO, so only the lines that - # name a fault should be emitted. - relay( - result.stderr, - repo_root, - stream=sys.stderr, - drop=lambda line: line.lstrip().startswith("INFO"), - ) - - if result.returncode != 0: - if not fix: - print( - f"hint: run 'python3 maint/lint/{SCRIPT}.py apply-all' to rewrite", - file=sys.stderr, - ) - return RETCODE_ERR - scope = ( - f"{len(only)} touched TOML file(s)" if only is not None - else "every TOML file" + def shorten(out: str, err: str) -> None: + relay(out, repo_root) + # Taplo reports the file count on stderr at INFO, so only the lines + # that name a fault should be emitted. + relay( + err, + repo_root, + stream=sys.stderr, + drop=lambda line: line.lstrip().startswith("INFO"), + ) + + # None, not an empty list: with no paths taplo finds its own through + # '.taplo.toml', so there is no count to report for the whole tree. + return formatted( + SCRIPT, + "TOML file", + None if only is None else [Path(name) for name in only], + lambda paths: [ + taplo, "fmt", + *([] if fix else ["--check", "--diff"]), + *[str(p) for p in paths], + ], + fix=fix, + scoped=only is not None, + cwd=repo_root, + output=shorten, ) - print(f"{SCRIPT}: rewrote {scope}" if fix else f"{SCRIPT}: {scope} conforms") - return RETCODE_PASS def _parse_version(text: str) -> Version: diff --git a/maint/lint/lint_codeql.py b/maint/lint/lint_codeql.py index 6d1d3b2b..79dce369 100755 --- a/maint/lint/lint_codeql.py +++ b/maint/lint/lint_codeql.py @@ -27,12 +27,13 @@ from collections.abc import Iterator from common import ( - DEFAULT_BASE, RETCODE_ERR, RETCODE_PASS, RETCODE_SKIP, SOURCE_DIRS, declare_verbs, + format_verbs, + formatted, require_bin, root_dir, touched, @@ -61,8 +62,14 @@ class Language(NamedTuple): pack: str # Binaries it needs; absent, the language skips rather than fails. requires: tuple[str, ...] + # Findings to drop, each matched whole as (path, message). + suppressions: tuple[tuple[str, str], ...] = () +_SUPPRESSIONS_RS = ( + ("pkgs/primitives/src/types/netinfo.rs", "Variable 'None' is not used."), +) + # Every language this harness knows, in the order `run-all` walks them. LANGUAGES: tuple[Language, ...] = ( Language( @@ -70,6 +77,7 @@ class Language(NamedTuple): directory="rust", pack="codeql/rust-queries", requires=("rustc",), + suppressions=_SUPPRESSIONS_RS, ), ) @@ -99,34 +107,19 @@ def _format_ql( only: list[str] | None = None, ) -> int: """Check or rewrite the formatting of the QL under `maint/codeql`.""" - sources = _ql_sources(repo_root, only) - if not sources: - print(f"{SCRIPT}: no QL file was touched") - return RETCODE_PASS - - result = subprocess.run( # noqa: S603 - [ + return formatted( + SCRIPT, + "QL file", + _ql_sources(repo_root, only), + lambda paths: [ codeql_bin, "query", "format", *(["-i"] if fix else ["--check-only"]), "--", - *[str(p) for p in sources], + *[str(p) for p in paths], ], - check=False, + fix=fix, + scoped=only is not None, ) - if result.returncode != 0: - if not fix: - print( - f"hint: run 'python3 maint/lint/{SCRIPT}.py apply-all' to rewrite", - file=sys.stderr, - ) - return RETCODE_ERR - - scope = ( - f"{len(sources)} touched QL file(s)" if only is not None - else f"every QL file ({len(sources)})" - ) - print(f"{SCRIPT}: rewrote {scope}" if fix else f"{SCRIPT}: {scope} conforms") - return RETCODE_PASS def _generate_source_lines( @@ -183,8 +176,11 @@ def _generate_source_lines( return out -def _print_csv_diagnostics(results_path: Path) -> int: - """Print CSV results to stderr. Returns the finding count.""" +def _print_csv_diagnostics( + results_path: Path, + suppressions: tuple[tuple[str, str], ...], +) -> int: + """Print CSV results to stderr. Returns the unsuppressed finding count.""" count = 0 with results_path.open(newline="") as f: for row in csv.reader(f): @@ -195,6 +191,8 @@ def _print_csv_diagnostics(results_path: Path) -> int: uri = Path(row[4].lstrip("/")) line = row[5] msg = row[3].replace("\n", " ") + if (str(uri), msg) in suppressions: + continue print(f"{uri}:{line}: {msg}", file=sys.stderr) count += 1 return count @@ -262,10 +260,7 @@ def _workspace_dirs( def _parse_args(argv: list[str]) -> argparse.Namespace: parser = declare_verbs( "Format the QL, and analyse the workspace with it.", - { - "check": "report every QL file whose formatting differs", - "apply": f"rewrite the QL this branch changed vs {DEFAULT_BASE}", - "apply-all": "rewrite every QL file in the tree", + format_verbs("QL file") | { "run": "analyse the one language --lang names", "run-all": "analyse every language whose tools are present", }, @@ -415,7 +410,10 @@ def _analyse( check=True, ) - total_findings = _print_csv_diagnostics(results_path) + total_findings = _print_csv_diagnostics( + results_path, + language.suppressions, + ) return RETCODE_ERR if total_findings > 0 else RETCODE_PASS diff --git a/maint/lint/lint_nix.py b/maint/lint/lint_nix.py new file mode 100755 index 00000000..a40f1356 --- /dev/null +++ b/maint/lint/lint_nix.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# coding: latin-1 + +# +# Copyright (c) 2026-present, The Dash Core developers +# SPDX-License-Identifier: MIT +# See the accompanying file LICENSE or https://opensource.org/license/MIT +# + +"""Check (and apply) RFC 166 styling for Nix definitions.""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +from common import ( + RETCODE_ERR, + RETCODE_SKIP, + declare_verbs, + format_verbs, + formatted, + is_plain_file, + require_bin, + root_dir, + touched, +) + +SCRIPT = Path(__file__).stem + + +def _sources(repo_root: Path, only: list[str] | None) -> list[Path]: + """Return tracked source files to lint or just *only* when given.""" + if only is not None: + return [repo_root / name for name in only] + git = require_bin("git") + listed = subprocess.run( # noqa: S603 + [git, "ls-files", "*.nix"], + capture_output=True, + check=True, + cwd=str(repo_root), + text=True, + ) + return [ + repo_root / name + for name in listed.stdout.splitlines() + if is_plain_file(repo_root, name) + ] + + +def main() -> int: + args = declare_verbs(__doc__ or "", format_verbs("Nix file")).parse_args() + + try: + nixfmt_bin = require_bin("nixfmt") + except FileNotFoundError as e: + print(f"{e}, skipping", file=sys.stderr) + return RETCODE_SKIP + + repo_root = root_dir() + fix = args.verb.startswith("apply") + only = touched(repo_root, (".nix",)) if args.verb == "apply" else None + sources = _sources(repo_root, only) + + return formatted( + SCRIPT, + "Nix file", + sources, + lambda paths: [ + nixfmt_bin, + *([] if fix else ["--check"]), + *[str(p) for p in paths], + ], + fix=fix, + scoped=only is not None, + cwd=repo_root, + ) + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception as exc: # noqa: BLE001 + print(exc, file=sys.stderr) + sys.exit(RETCODE_ERR) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 2de0c03a..2475f574 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] channel = "nightly-2026-02-01" -components = ["clippy", "rust-analyzer", "rustfmt"] -profile = "default" +components = ["clippy", "llvm-tools", "rust-analyzer", "rust-src", "rustfmt"] +profile = "minimal"