diff --git a/.github/actions/setup-nix/action.yml b/.github/actions/setup-nix/action.yml new file mode 100644 index 0000000000..3d177abd99 --- /dev/null +++ b/.github/actions/setup-nix/action.yml @@ -0,0 +1,28 @@ +name: 'Setup with Nix' +description: 'Install Nix and prepare devshell for Light Protocol' + +inputs: + skip-devshell-build: + description: 'Skip building the devshell (for faster startup when only nix tools needed)' + required: false + default: 'false' + +runs: + using: 'composite' + steps: + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + + - name: Enable Nix cache + uses: DeterminateSystems/magic-nix-cache-action@main + + - name: Build devshell + if: inputs.skip-devshell-build != 'true' + shell: bash + run: | + nix develop ./nix/devenv --command echo "Devshell ready" + + - name: Set environment + shell: bash + run: | + echo "IN_NIX_SHELL=true" >> $GITHUB_ENV diff --git a/.github/workflows/nix-test.yml b/.github/workflows/nix-test.yml new file mode 100644 index 0000000000..a758e9b958 --- /dev/null +++ b/.github/workflows/nix-test.yml @@ -0,0 +1,44 @@ +name: Nix Build Test + +permissions: + contents: read + +on: + pull_request: + paths: + - 'nix/**' + - '.github/workflows/nix-test.yml' + - '.github/actions/setup-nix/**' + workflow_dispatch: + +jobs: + test-nix-devshell: + name: Test Nix Devshell + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Nix + uses: ./.github/actions/setup-nix + + - name: Verify tools + run: | + nix develop ./nix/devenv --command bash -c ' + echo "=== Tool Versions ===" + solana --version + anchor --version + go version + node --version + pnpm --version + echo "=== All tools working ===" + ' + + - name: Test cargo build-sbf + run: | + nix develop ./nix/devenv --command bash -c ' + cd programs/system + cargo build-sbf 2>&1 | tail -5 + ' diff --git a/.gitignore b/.gitignore index 48f4fbd9be..a20f8bdaba 100644 --- a/.gitignore +++ b/.gitignore @@ -95,3 +95,6 @@ output1.txt **/~/ expand.rs + +# Separate target dirs for SBF builds (pinocchio vs anchor features) +target-*/ diff --git a/nix/devenv/anchor.nix b/nix/devenv/anchor.nix new file mode 100644 index 0000000000..030c4b97ac --- /dev/null +++ b/nix/devenv/anchor.nix @@ -0,0 +1,69 @@ +{ lib +, stdenv +, fetchurl +, autoPatchelfHook +, zlib +, openssl +, glibc +}: + +let + version = "0.31.1"; + + sources = { + x86_64-linux = { + url = "https://github.com/coral-xyz/anchor/releases/download/v${version}/anchor-${version}-x86_64-unknown-linux-gnu"; + hash = "sha256-Xl+PwPdfLD3FzOsIKn9zXQm+IgdUApH/rTcOtbLclZs="; + }; + x86_64-darwin = { + url = "https://github.com/coral-xyz/anchor/releases/download/v${version}/anchor-${version}-x86_64-apple-darwin"; + hash = "sha256-MwGcRwS2x4toDyth5yJEiXKsZ6vokBnyCdCSAGhPTLs="; + }; + aarch64-darwin = { + url = "https://github.com/coral-xyz/anchor/releases/download/v${version}/anchor-${version}-aarch64-apple-darwin"; + hash = "sha256-ljxesAeqMwTXDV4H+Ng5AqHLiQLv7l3aRHzlSLk3lJQ="; + }; + }; + + platform = stdenv.hostPlatform.system; + src = fetchurl { + inherit (sources.${platform}) url hash; + }; + +in stdenv.mkDerivation { + pname = "anchor"; + inherit version src; + + dontUnpack = true; + + nativeBuildInputs = lib.optionals stdenv.isLinux [ + autoPatchelfHook + ]; + + buildInputs = lib.optionals stdenv.isLinux [ + zlib + openssl + stdenv.cc.cc.lib + ]; + + dontConfigure = true; + dontBuild = true; + + installPhase = '' + runHook preInstall + mkdir -p $out/bin + cp $src $out/bin/anchor + chmod +x $out/bin/anchor + runHook postInstall + ''; + + meta = with lib; { + description = "Anchor framework for Solana"; + homepage = "https://anchor-lang.com"; + license = licenses.asl20; + platforms = builtins.attrNames sources; + mainProgram = "anchor"; + }; + + passthru = { inherit version; }; +} diff --git a/nix/devenv/flake.lock b/nix/devenv/flake.lock new file mode 100644 index 0000000000..2a707014ff --- /dev/null +++ b/nix/devenv/flake.lock @@ -0,0 +1,96 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1766070988, + "narHash": "sha256-G/WVghka6c4bAzMhTwT2vjLccg/awmHkdKSd2JrycLc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c6245e83d836d0433170a16eb185cefe0572f8b8", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1744536153, + "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1766285238, + "narHash": "sha256-DqVXFZ4ToiFHgnxebMWVL70W+U+JOxpmfD37eWD/Qc8=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "c4249d0c370d573d95e33b472014eae4f2507c2f", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/nix/devenv/flake.nix b/nix/devenv/flake.nix new file mode 100644 index 0000000000..0f5e326722 --- /dev/null +++ b/nix/devenv/flake.nix @@ -0,0 +1,166 @@ +{ + description = "Light Protocol development environment"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + rust-overlay.url = "github:oxalica/rust-overlay"; + }; + + outputs = { self, nixpkgs, flake-utils, rust-overlay }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ rust-overlay.overlays.default ]; + }; + + # Versions (keep in sync with scripts/devenv/versions.sh) + rustVersion = "1.90.0"; + photonCommit = "3dbfb8e6772779fc89c640b5b0823b95d1958efc"; + + # Rust toolchains from rust-overlay (pre-built binaries, fast) + rustStable = pkgs.rust-bin.stable.${rustVersion}.default.override { + extensions = [ "clippy" "rust-src" ]; + }; + rustNightlyFmt = pkgs.rust-bin.nightly.latest.rustfmt; + + # Import custom packages + solana = pkgs.callPackage ./solana.nix { }; + anchor = pkgs.callPackage ./anchor.nix { }; + + # Smart build-sbf wrapper that uses per-program target dirs to avoid cache invalidation + buildSbfWrapper = pkgs.writeShellScriptBin "build-sbf" '' + set -e + REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" + + build_program() { + local prog="$1"; shift + local name=$(basename "$prog") + echo "==> $prog" + CARGO_TARGET_DIR="$REPO_ROOT/target-$name" cargo build-sbf --manifest-path "$prog/Cargo.toml" "$@" + } + + if [ -f "Cargo.toml" ] && grep -q '^\[workspace\]' "Cargo.toml" 2>/dev/null; then + echo "Building programs with separate target directories..." + for prog in programs/*/; do + [ -f "$prog/Cargo.toml" ] && build_program "$prog" "$@" + done + else + name=$(basename "$PWD") + export CARGO_TARGET_DIR="$REPO_ROOT/target-$name" + exec cargo build-sbf "$@" + fi + ''; + + # Smart test-sbf wrapper + testSbfWrapper = pkgs.writeShellScriptBin "test-sbf" '' + set -e + REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" + + test_program() { + local prog="$1"; shift + local name=$(basename "$prog") + echo "==> $prog" + CARGO_TARGET_DIR="$REPO_ROOT/target-$name" cargo test-sbf --manifest-path "$prog/Cargo.toml" "$@" + } + + if [ -f "Cargo.toml" ] && grep -q '^\[workspace\]' "Cargo.toml" 2>/dev/null; then + echo "Testing programs with separate target directories..." + for prog in programs/*/; do + [ -f "$prog/Cargo.toml" ] && test_program "$prog" "$@" + done + else + name=$(basename "$PWD") + export CARGO_TARGET_DIR="$REPO_ROOT/target-$name" + exec cargo test-sbf "$@" + fi + ''; + + in { + packages = { + inherit solana anchor; + default = solana; + }; + + devShells.default = pkgs.mkShell { + name = "light"; + + packages = [ + # Languages + pkgs.go + rustStable + rustNightlyFmt # For cargo fmt + pkgs.nodejs_22 + pkgs.pnpm + + # Tools + pkgs.jq + pkgs.redis + pkgs.gnumake + pkgs.pkg-config + pkgs.openssl + pkgs.starship + + # Solana ecosystem + solana + anchor + buildSbfWrapper # Smart wrapper: build-sbf + testSbfWrapper # Smart wrapper: test-sbf + ]; + + shellHook = '' + export REDIS_URL="redis://localhost:6379" + export SBF_OUT_DIR="target/deploy" + + # Solana platform-tools: copy SDK to writable location for cargo-build-sbf + SOLANA_TOOLS_DIR="$HOME/.cache/solana-platform-tools/${solana.version}" + if [ ! -d "$SOLANA_TOOLS_DIR/sbf" ]; then + echo "Setting up Solana platform-tools SDK..." + mkdir -p "$SOLANA_TOOLS_DIR" + cp -r ${solana}/bin/platform-tools-sdk/* "$SOLANA_TOOLS_DIR/" + chmod -R u+w "$SOLANA_TOOLS_DIR" + fi + export SBF_SDK_PATH="$SOLANA_TOOLS_DIR/sbf" + + # Photon indexer (installed via cargo) + if ! command -v photon &>/dev/null; then + echo "Installing Photon indexer..." + RUSTFLAGS="-A dead-code" cargo install \ + --git https://github.com/helius-labs/photon.git \ + --rev ${photonCommit} \ + --locked + fi + + # Gnark proving keys (use absolute path) + KEYS_DIR="$(pwd)/prover/server/proving-keys" + if [ ! -d "$KEYS_DIR" ] || [ -z "$(ls -A "$KEYS_DIR" 2>/dev/null)" ]; then + echo "Downloading gnark proving keys..." + (cd prover/server && go run . download --run-mode=forester-test --keys-dir="$KEYS_DIR" --max-retries=10) + fi + + # Node dependencies + if [ ! -d "node_modules" ] || [ -z "$(ls -A node_modules 2>/dev/null)" ]; then + echo "Installing node dependencies..." + pnpm install + fi + + # Mark that we're in the devenv (for custom prompts) + export LIGHT_DEVENV=1 + + # Initialize starship prompt if available and shell is interactive + if [[ $- == *i* ]] && command -v starship &>/dev/null; then + eval "$(starship init bash 2>/dev/null || starship init zsh 2>/dev/null || true)" + fi + + echo "" + echo "Light Protocol devenv activated" + echo " Solana: ${solana.version}" + echo " Anchor: ${anchor.version}" + echo " Rust: ${rustVersion}" + echo "" + ''; + + }; + }); +} diff --git a/nix/devenv/solana.nix b/nix/devenv/solana.nix new file mode 100644 index 0000000000..1e0c2da6a2 --- /dev/null +++ b/nix/devenv/solana.nix @@ -0,0 +1,81 @@ +{ lib +, stdenv +, fetchurl +, autoPatchelfHook +, zlib +, openssl +, udev +, libclang +, llvmPackages +}: + +let + version = "2.2.15"; + + sources = { + x86_64-linux = { + url = "https://github.com/anza-xyz/agave/releases/download/v${version}/solana-release-x86_64-unknown-linux-gnu.tar.bz2"; + hash = "sha256-KfOtGQo9sjB+ZiH0Q0qSXBsQJe8I1Ydr+lqDKanopX4="; + }; + x86_64-darwin = { + url = "https://github.com/anza-xyz/agave/releases/download/v${version}/solana-release-x86_64-apple-darwin.tar.bz2"; + hash = "sha256-uCnX3MkGf3oxKNna8U7+GA4ux9E8Mcsb1WDpkdZc8sQ="; + }; + aarch64-darwin = { + url = "https://github.com/anza-xyz/agave/releases/download/v${version}/solana-release-aarch64-apple-darwin.tar.bz2"; + hash = "sha256-4ycCeVg/EenfWwLO0erK2ryTQ4VSXNWk3nw+W8WQjX8="; + }; + }; + + platform = stdenv.hostPlatform.system; + src = fetchurl { + inherit (sources.${platform}) url hash; + }; + +in stdenv.mkDerivation { + pname = "solana-cli"; + inherit version src; + + sourceRoot = "."; + + nativeBuildInputs = lib.optionals stdenv.isLinux [ + autoPatchelfHook + ]; + + buildInputs = lib.optionals stdenv.isLinux [ + zlib + openssl + stdenv.cc.cc.lib + ] ++ lib.optionals (stdenv.isLinux && udev != null) [ + udev + ]; + + dontConfigure = true; + dontBuild = true; + + # Ignore missing CUDA/SGX libraries - they're optional performance libs + autoPatchelfIgnoreMissingDeps = [ + "libOpenCL.so.1" + "libsgx_uae_service.so" + "libsgx_urts.so" + ]; + + installPhase = '' + runHook preInstall + mkdir -p $out/bin + cp -r solana-release/bin/* $out/bin/ + # Remove optional perf-libs that require CUDA/SGX (not needed for dev/CI) + rm -rf $out/bin/perf-libs + runHook postInstall + ''; + + meta = with lib; { + description = "Solana CLI tools"; + homepage = "https://solana.com"; + license = licenses.asl20; + platforms = builtins.attrNames sources; + mainProgram = "solana"; + }; + + passthru = { inherit version; }; +} diff --git a/scripts/build-sbf.sh b/scripts/build-sbf.sh new file mode 100755 index 0000000000..0c372a0628 --- /dev/null +++ b/scripts/build-sbf.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# +# Smart cargo build-sbf wrapper that uses per-program target directories +# to avoid cache invalidation from different feature combinations. +# +# Usage: +# From workspace root: ./scripts/build-sbf.sh (builds all programs) +# From program dir: ./scripts/build-sbf.sh (builds current program) +# Or add to PATH and use: build-sbf +# + +set -e + +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" + +# Build a single program with its own target dir +build_program() { + local prog="$1" + shift + local name=$(basename "$prog") + echo "==> Building $prog" + CARGO_TARGET_DIR="$REPO_ROOT/target-$name" cargo build-sbf --manifest-path "$prog/Cargo.toml" "$@" +} + +# If running from workspace root, build each program separately +if [ -f "Cargo.toml" ] && grep -q '^\[workspace\]' "Cargo.toml" 2>/dev/null; then + echo "Building programs with separate target directories..." + for prog in programs/*/; do + if [ -f "$prog/Cargo.toml" ]; then + build_program "$prog" "$@" + fi + done +else + # Single program build - use program name for target dir + name=$(basename "$PWD") + export CARGO_TARGET_DIR="$REPO_ROOT/target-$name" + echo "==> Building $name (target: $CARGO_TARGET_DIR)" + exec cargo build-sbf "$@" +fi diff --git a/scripts/test-sbf.sh b/scripts/test-sbf.sh new file mode 100755 index 0000000000..7a088536eb --- /dev/null +++ b/scripts/test-sbf.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# +# Smart cargo test-sbf wrapper that uses per-program target directories +# to avoid cache invalidation from different feature combinations. +# +# Usage: +# From workspace root: ./scripts/test-sbf.sh (tests all programs) +# From program dir: ./scripts/test-sbf.sh (tests current program) +# + +set -e + +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" + +# Test a single program with its own target dir +test_program() { + local prog="$1" + shift + local name=$(basename "$prog") + echo "==> Testing $prog" + CARGO_TARGET_DIR="$REPO_ROOT/target-$name" cargo test-sbf --manifest-path "$prog/Cargo.toml" "$@" +} + +# If running from workspace root, test each program separately +if [ -f "Cargo.toml" ] && grep -q '^\[workspace\]' "Cargo.toml" 2>/dev/null; then + echo "Testing programs with separate target directories..." + for prog in programs/*/; do + if [ -f "$prog/Cargo.toml" ]; then + test_program "$prog" "$@" + fi + done +else + # Single program test - use program name for target dir + name=$(basename "$PWD") + export CARGO_TARGET_DIR="$REPO_ROOT/target-$name" + echo "==> Testing $name (target: $CARGO_TARGET_DIR)" + exec cargo test-sbf "$@" +fi