diff --git a/.github/workflows/python-release.yml b/.github/workflows/python-release.yml index 614982892..ad7978407 100644 --- a/.github/workflows/python-release.yml +++ b/.github/workflows/python-release.yml @@ -192,9 +192,9 @@ jobs: CIBW_MANYLINUX_AARCH64_IMAGE: "manylinux_2_28" # Linux configuration - GCC Toolset and CUDA paths are conditional via matrix variables CIBW_ENVIRONMENT_LINUX: > - PATH=${{ matrix.gcc_path_prefix }}$HOME/.cargo/bin:$HOME/.pecos/llvm/bin:/usr/local/cuda-12.6/bin:$PATH + PATH=${{ matrix.gcc_path_prefix }}$HOME/.cargo/bin:$HOME/.pecos/deps/llvm/bin:/usr/local/cuda-12.6/bin:$PATH LD_LIBRARY_PATH=${{ matrix.gcc_ld_path }}$LD_LIBRARY_PATH - LLVM_SYS_140_PREFIX=$HOME/.pecos/llvm + LLVM_SYS_140_PREFIX=$HOME/.pecos/deps/llvm CUDA_PATH=/usr/local/cuda-12.6 MATURIN_PEP517_ARGS="${{ matrix.cuda_feature }}" CIBW_BEFORE_ALL_LINUX: | @@ -223,8 +223,8 @@ jobs: pipx run abi3audit --strict --report {wheel} # macOS configuration CIBW_ENVIRONMENT_MACOS: > - PATH=$HOME/.cargo/bin:$HOME/.pecos/llvm/bin:$PATH - LLVM_SYS_140_PREFIX=$HOME/.pecos/llvm + PATH=$HOME/.cargo/bin:$HOME/.pecos/deps/llvm/bin:$PATH + LLVM_SYS_140_PREFIX=$HOME/.pecos/deps/llvm MACOSX_DEPLOYMENT_TARGET=13.2 CIBW_BEFORE_ALL_MACOS: | curl -sSf https://sh.rustup.rs | sh -s -- -y @@ -237,12 +237,12 @@ jobs: printf '#!/bin/bash\nunset DYLD_LIBRARY_PATH\nexec /usr/bin/codesign "$@"\n' > $HOME/.pecos/bin/codesign chmod +x $HOME/.pecos/bin/codesign CIBW_REPAIR_WHEEL_COMMAND_MACOS: > - PATH=$HOME/.pecos/bin:$PATH DYLD_LIBRARY_PATH=$HOME/.pecos/llvm/lib delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel} && + PATH=$HOME/.pecos/bin:$PATH DYLD_LIBRARY_PATH=$HOME/.pecos/deps/llvm/lib delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel} && pipx run abi3audit --strict --report {wheel} # Windows configuration - CUDA via Jimver/cuda-toolkit (installed before cibuildwheel) CIBW_ENVIRONMENT_WINDOWS: > - PATH="C:\\Users\\runneradmin\\.pecos\\llvm\\bin;$PATH" - LLVM_SYS_140_PREFIX="C:\\Users\\runneradmin\\.pecos\\llvm" + PATH="C:\\Users\\runneradmin\\.pecos\\deps\\llvm\\bin;$PATH" + LLVM_SYS_140_PREFIX="C:\\Users\\runneradmin\\.pecos\\deps\\llvm" MATURIN_PEP517_ARGS="${{ matrix.cuda_feature }}" CIBW_BEFORE_ALL_WINDOWS: > echo "=== Installing LLVM using pecos ===" && @@ -250,9 +250,9 @@ jobs: echo "=== Running pecos install llvm ===" && cargo run --release -p pecos --features cli -- install llvm --force && echo "=== Checking LLVM installation ===" && - (test -d "C:\\Users\\runneradmin\\.pecos\\llvm" && echo "LLVM directory exists" && ls -la "C:\\Users\\runneradmin\\.pecos\\llvm" && (ls -la "C:\\Users\\runneradmin\\.pecos\\llvm\\bin" || echo "bin directory not found")) || (echo "ERROR: LLVM directory not found!" && exit 1) && + (test -d "C:\\Users\\runneradmin\\.pecos\\deps\\llvm" && echo "LLVM directory exists" && ls -la "C:\\Users\\runneradmin\\.pecos\\deps\\llvm" && (ls -la "C:\\Users\\runneradmin\\.pecos\\deps\\llvm\\bin" || echo "bin directory not found")) || (echo "ERROR: LLVM directory not found!" && exit 1) && echo "=== Verifying LLVM_SYS_140_PREFIX ===" && - echo "LLVM_SYS_140_PREFIX will be set to: C:\\Users\\runneradmin\\.pecos\\llvm" + echo "LLVM_SYS_140_PREFIX will be set to: C:\\Users\\runneradmin\\.pecos\\deps\\llvm" # Install delvewheel and patch it to ignore ext-ms-win-* API sets # (delvewheel ignores api-ms-win-* but not ext-ms-win-* which are also Windows API sets) CIBW_BEFORE_BUILD_WINDOWS: > @@ -261,7 +261,7 @@ jobs: # Note: --no-dll excludes Windows system DLLs that should not be bundled # combase.dll and rmclient.dll are core Windows components that fail when bundled CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: > - delvewheel repair -v --add-path "C:\\Users\\runneradmin\\.pecos\\llvm\\bin" --no-dll "combase.dll;rmclient.dll" -w {dest_dir} {wheel} && + delvewheel repair -v --add-path "C:\\Users\\runneradmin\\.pecos\\deps\\llvm\\bin" --no-dll "combase.dll;rmclient.dll" -w {dest_dir} {wheel} && pipx run abi3audit --strict --report {wheel} - name: Upload wheels diff --git a/Justfile b/Justfile index f751fe6da..8e60b9749 100644 --- a/Justfile +++ b/Justfile @@ -3,9 +3,20 @@ # Install: cargo install just # Usage: just or just --list -# Default recipe: show help +# Default recipe: show quick-start guide + recipe list default: - @just --list + @echo "PECOS Development" + @echo "=================" + @echo "" + @echo "Quick start:" + @echo " just install-cli # First time: install the pecos CLI" + @echo " just build # Build PECOS (auto-installs dependencies)" + @echo " just test # Run all tests" + @echo " just dev # Build + test (daily workflow)" + @echo " just lint # Check formatting and linting" + @echo "" + @echo "All commands:" + @just --list --list-heading '' # ============================================================================= # Settings @@ -19,273 +30,118 @@ set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] pecos := "pecos" # ============================================================================= -# Requirements -# ============================================================================= - -# Generate/update lockfiles -updatereqs: - @echo "Ensuring uv is installed..." - uv self update - @echo "Generating lock files..." - uv lock --project . - -# Install Python project requirements to root .venv -# Automatically includes cuda group if CUDA packages were previously installed -installreqs: - #!/usr/bin/env bash - set -euo pipefail - echo "Installing requirements..." - if python -c "import cupy" >/dev/null 2>&1; then - echo "(including CUDA packages)" - uv sync --project . --all-packages --group cuda - else - uv sync --project . --all-packages - fi - -# Install requirements with specific Python version -installreqs-python version: - #!/usr/bin/env bash - set -euo pipefail - echo "Installing requirements with Python {{version}}..." - if python -c "import cupy" >/dev/null 2>&1; then - echo "(including CUDA packages)" - uv sync --project . --all-packages --python "{{version}}" --group cuda - else - uv sync --project . --all-packages --python "{{version}}" - fi - -# ============================================================================= -# PECOS CLI +# Getting Started # ============================================================================= -# Check if PECOS CLI is installed, fail with helpful message if not -[private] -check-cli: - #!/usr/bin/env bash - if ! command -v pecos >/dev/null 2>&1; then - echo "" - echo "Error: PECOS CLI not found." - echo "" - echo "Install it with:" - echo " just install-cli" - echo "" - echo "Or manually:" - echo " cargo install --path crates/pecos --features cli" - echo "" - exit 1 - fi - - # Check if the installed CLI might be stale - stale=false - reasons=() - - # Check 1: version mismatch - expected_version=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') - installed_version=$(pecos --version 2>/dev/null | awk '{print $2}') - if [[ "$installed_version" != "$expected_version" ]]; then - stale=true - reasons+=("Version mismatch (installed: ${installed_version:-unknown}, expected: $expected_version)") - fi - - # Check 2: missing expected subcommands - if ! pecos rust --help >/dev/null 2>&1; then - stale=true - reasons+=("Missing 'rust' subcommand") - fi - - if [[ "$stale" == "true" ]]; then - echo "" - echo "Warning: PECOS CLI may be outdated." - for reason in "${reasons[@]}"; do - echo " - $reason" - done - echo "" - echo " Update with: just reinstall-cli" - echo "" - fi - - # Informational: suggest CUDA Python packages if toolkit available but cupy isn't - if pecos cuda check -q >/dev/null 2>&1; then - if ! python -c "import cupy" >/dev/null 2>&1; then - echo "" - echo "Note: CUDA toolkit detected but Python CUDA packages not installed." - echo " To enable GPU-accelerated simulations:" - echo " pecos cuda setup-python" - echo " Or manually:" - echo " uv sync --group cuda" - echo "" - fi - fi - # Install PECOS CLI (required for most recipes) +[group('setup')] install-cli: @echo "Installing PECOS CLI..." cargo install --path crates/pecos --features cli @echo "" - @echo "Done! You can now run: just dev" + @echo "Done! You can now run: just build" # Reinstall PECOS CLI (run after changing CLI code) +[group('setup')] reinstall-cli: - @echo "Reinstalling PECOS CLI..." cargo install --path crates/pecos --features cli --force - @echo "" - @echo "Done!" - -# ============================================================================= -# LLVM Setup -# ============================================================================= - -# Install LLVM 14 to ~/.pecos/llvm/ (required for QIR features) -install-llvm: - @echo "Installing LLVM 14..." - {{pecos}} install llvm - -# Check LLVM 14 installation status -check-llvm: - -{{pecos}} llvm check - -# Configure LLVM paths in .cargo/config.toml -configure-llvm: - {{pecos}} llvm configure - -# ============================================================================= -# CUDA Setup -# ============================================================================= - -# Install CUDA Toolkit to ~/.pecos/cuda/ (for GPU support, no GPU needed) -install-cuda: - @echo "Installing CUDA Toolkit..." - {{pecos}} install cuda -# Check CUDA installation status (local or system) -check-cuda: - -{{pecos}} cuda check +# Set up build environment (detect and install missing dependencies) +[group('setup')] +setup: check-cli + {{pecos}} setup -# Validate CUDA installation integrity -validate-cuda: - {{pecos}} cuda validate +# Set up build environment, accepting all prompts (for CI) +[group('setup')] +setup-ci: check-cli + {{pecos}} setup --yes -# Install CUDA Python packages (cupy, cuquantum, pytket-cutensornet) -# Requires CUDA toolkit to be installed first (just install-cuda or system CUDA) -install-cuda-python: - {{pecos}} cuda setup-python +# Show system information +[group('setup')] +sys-info: check-cli + {{pecos}} sys-info -# Full CUDA setup: toolkit + Python packages -setup-cuda: install-cuda install-cuda-python - @echo "Full CUDA setup complete (toolkit + Python packages)" +# List installed and cached dependencies +[group('setup')] +list-deps: check-cli + {{pecos}} list -v # ============================================================================= # Building # ============================================================================= # Build PECOS (profile: debug, release, native) -build profile="debug": check-cli installreqs build-selene +[group('build')] +build profile="debug": check-cli setup-quiet installreqs build-selene {{pecos}} python build --profile {{profile}} - # Build FFI crates if tools available (- prefix ignores errors) -{{pecos}} julia build --profile {{profile}} -{{pecos}} go build --profile {{profile}} -# Build and install Selene plugins for development -build-selene: - #!/usr/bin/env bash - set -euo pipefail - echo "Building Selene plugins..." - - # Discover and build all selene plugins - CARGO_ARGS="" - for DIR in python/selene-plugins/pecos-selene-*/; do - CARGO_ARGS="$CARGO_ARGS -p $(basename "$DIR")" - done - if [ -n "$CARGO_ARGS" ]; then - cargo build --release $CARGO_ARGS - fi - - # Copy libraries to Python package directories - echo "Copying libraries to Python packages..." - {{pecos}} selene install --profile release - - # Selene plugins are workspace members, so uv sync --all-packages handles editable installs - echo "Selene plugins built and installed successfully" +# Build PECOS without dependency setup prompts +[group('build')] +build-lite profile="debug": check-cli installreqs build-selene + {{pecos}} python build --profile {{profile}} + -{{pecos}} julia build --profile {{profile}} + -{{pecos}} go build --profile {{profile}} # Build PECOS with CUDA support -build-cuda profile="debug": installreqs +[group('build')] +build-cuda profile="debug": check-cli setup-quiet installreqs {{pecos}} python build --profile {{profile}} --cuda - # Build FFI crates if tools available (- prefix ignores errors) -{{pecos}} julia build --profile {{profile}} -{{pecos}} go build --profile {{profile}} -# Convenience aliases -build-debug: (build "debug") -build-release: (build "release") -build-native: (build "native") -build-cuda-debug: (build-cuda "debug") -build-cuda-release: (build-cuda "release") -build-cuda-native: (build-cuda "native") - # ============================================================================= -# Documentation +# Testing # ============================================================================= -# Build documentation -docs-build: - uv run mkdocs build --clean - -# Serve documentation and open in browser -docs port="8000": check-cli - {{pecos}} docs --port {{port}} - -# Test Python code examples in documentation (excludes slow tests and Rust tests) -docs-test: - uv run python scripts/docs/generate_doc_tests.py - uv run pytest python/quantum-pecos/tests/docs/generated -v -k "not rust" -m "not slow" - -# Test all Python code examples including slow tests (transversal CNOT - takes >2 hours) -docs-test-slow: - uv run python scripts/docs/generate_doc_tests.py - uv run pytest python/quantum-pecos/tests/docs/generated -v -k "not rust" +# Run Python tests (core) +[group('test')] +pytest: + uv run pytest python/pecos-rslib/tests -m "not performance and not numpy" + uv run pytest python/quantum-pecos/tests -m "not optional_dependency and not numpy" -# Generate doc tests without running them -docs-test-generate: - uv run python scripts/docs/generate_doc_tests.py +# Run Rust tests (CUDA-aware, release mode) +[group('test')] +rstest: check-cli + {{pecos}} rust test --release -# Run doc tests with pytest options (e.g., just docs-test-run "-k bell_state") -docs-test-run *args: - uv run pytest python/quantum-pecos/tests/docs/generated {{args}} +# Run all tests (Rust + Python + Julia + Go if available) +[group('test')] +test: rstest-all pytest-all + #!/usr/bin/env bash + set -euo pipefail + if command -v julia >/dev/null 2>&1; then + echo "Julia detected, running Julia tests..." + {{pecos}} julia test + else + echo "Julia not detected, skipping Julia tests" + fi + if command -v go >/dev/null 2>&1; then + echo "Go detected, running Go tests..." + {{pecos}} go test + else + echo "Go not detected, skipping Go tests" + fi -# Legacy: test code examples with old script -docs-test-legacy: - uv run python scripts/docs/test_code_examples.py +# Run all Python tests (core + numpy compat + selene) +[group('test')] +pytest-all: pytest pytest-numpy pytest-selene + @echo "All Python tests completed" # ============================================================================= # Linting / Formatting # ============================================================================= -# Run cargo check (with GPU features only if CUDA available) -check: check-cli - {{pecos}} rust check --include-ffi - -# Run cargo clippy (with GPU features only if CUDA available) -clippy: check-cli - @echo "==> Running clippy via pecos..." - {{pecos}} rust clippy --include-ffi - -# Check Rust formatting (without fixing) -fmt: check-cli - @echo "==> Running fmt check via pecos..." - {{pecos}} rust fmt --check - -# Fix Rust formatting issues -fmt-fix: check-cli - {{pecos}} rust fmt - -# Run all quality checks / linting (check only) +# Run all quality checks (fmt + clippy + pre-commit + Julia/Go if available) +[group('lint')] lint: fmt clippy #!/usr/bin/env bash set -euo pipefail echo "==> Running pre-commit..." uv run pre-commit run --all-files - if {{pecos}} julia check -q >/dev/null 2>&1; then + if command -v julia >/dev/null 2>&1; then echo "Julia detected, running Julia formatting check and linting..." {{pecos}} julia fmt --check {{pecos}} julia lint @@ -293,354 +149,354 @@ lint: fmt clippy echo "Julia not detected, skipping Julia linting" fi - if {{pecos}} go check -q >/dev/null 2>&1; then + if command -v go >/dev/null 2>&1; then echo "Go detected, running Go formatting check and linting..." - {{pecos}} go fmt --check - {{pecos}} go lint + test -z "$(gofmt -l go/pecos)" || (gofmt -l go/pecos && exit 1) + cd go/pecos && go vet ./... else echo "Go not detected, skipping Go linting" fi -# Fix all auto-fixable linting issues (Rust, Python, Julia, Go) +# Fix all auto-fixable linting issues +[group('lint')] lint-fix: #!/usr/bin/env bash set -euo pipefail echo "Fixing Rust formatting and clippy issues..." - {{pecos}} rust fmt - {{pecos}} rust clippy --fix --include-ffi + cargo fmt --all + cargo clippy --workspace --all-targets --fix --allow-staged --allow-dirty -- -D warnings echo "" echo "Running pre-commit fixes..." uv run pre-commit run --all-files || true echo "" - - if {{pecos}} julia check -q >/dev/null 2>&1; then + if command -v julia >/dev/null 2>&1; then echo "Fixing Julia formatting..." {{pecos}} julia fmt - echo "" - echo "Note: Some Julia linting issues from Aqua.jl may require manual fixes." else echo "Julia not detected, skipping Julia formatting" fi - - if {{pecos}} go check -q >/dev/null 2>&1; then + if command -v go >/dev/null 2>&1; then echo "Fixing Go formatting..." - {{pecos}} go fmt + gofmt -w go/pecos else echo "Go not detected, skipping Go formatting" fi echo "" echo "Linting fixes applied! Run 'just lint' to check for remaining issues." -# Normalize line endings according to .gitattributes -normalize-line-endings: - @echo "Normalizing line endings according to .gitattributes..." - @echo "This will refresh all tracked files to apply .gitattributes rules" - -git rm --cached -r . - git reset --hard - @echo "Line endings normalized. Check 'git status' for any changes." +# Run cargo check +[group('lint')] +check: + cargo check --workspace --all-targets -# ============================================================================= -# Testing -# ============================================================================= +# Run cargo clippy +[group('lint')] +clippy: + cargo clippy --workspace --all-targets -- -D warnings -# Run Rust tests. -# Includes pecos-gpu-sims when the GPU probe succeeds. -rstest: check-cli - {{pecos}} rust test --release +# Check Rust formatting +[group('lint')] +fmt: + cargo fmt --all -- --check -# Run Rust tests in the default cargo profile. -# Includes pecos-gpu-sims when the GPU probe succeeds. -rstest-all: check-cli - {{pecos}} rust test +# Fix Rust formatting +[group('lint')] +fmt-fix: + cargo fmt --all -# Run Python tests (excluding numpy and optional deps) -pytest: check-cli - {{pecos}} python test +# ============================================================================= +# Dev Workflows +# ============================================================================= -# Run NumPy/SciPy compatibility tests -pytest-numpy: check-cli - {{pecos}} python test --numpy +# Dev cycle: build + test (fast, for normal development) +[group('dev')] +dev cuda="false": pre-check (build-dev cuda) test -# Run performance tests with release build -pytest-perf: build-release - @echo "Running pecos-rslib performance tests with release build..." - uv run --group numpy-compat pytest ./python/pecos-rslib/tests/ -m "performance" -v +# Full dev cycle: clean + build + test + lint (pre-merge) +[group('dev')] +dev-full cuda="false": pre-check clean (build-dev cuda) test lint -# Run tests for optional dependencies -pytest-dep: check-cli - {{pecos}} python test -m optional_dependency +# Dev cycle with CUDA support +[group('dev')] +devc: (dev "true") -# Run Selene plugin tests -pytest-selene: check-cli - {{pecos}} python test --selene +# Full dev cycle with CUDA support +[group('dev')] +devc-full: (dev-full "true") -# Run all Python tests (core + numpy compat + selene) -pytest-all: pytest pytest-numpy pytest-selene - @echo "All Python tests completed (core + NumPy/SciPy compatibility + Selene plugins)" +# Clean build artifacts +[group('dev')] +[group('clean')] +clean: + uv run python scripts/clean.py -# Run all tests (Rust + Python + Julia + Go if available) -test: rstest-all pytest-all - #!/usr/bin/env bash - set -euo pipefail - if {{pecos}} julia check -q >/dev/null 2>&1; then - echo "Julia detected, running Julia tests..." - {{pecos}} julia test - else - echo "Julia not detected, skipping Julia tests" - fi +# ============================================================================= +# Documentation +# ============================================================================= - if {{pecos}} go check -q >/dev/null 2>&1; then - echo "Go detected, running Go tests..." - {{pecos}} go test - else - echo "Go not detected, skipping Go tests" - fi +# Serve documentation and open in browser +[group('docs')] +docs port="8000": check-cli + {{pecos}} docs --port {{port}} -# Run all tests with warnings for missing tools -test-all: rstest-all pytest-all - #!/usr/bin/env bash - set -euo pipefail - if {{pecos}} julia check -q >/dev/null 2>&1; then - echo "Julia detected, running Julia tests..." - {{pecos}} julia test - else - echo "" - echo "WARNING: Julia is not installed. Skipping Julia tests." - echo " To run Julia tests, please install Julia from https://julialang.org/downloads/" - echo "" - fi +# Build documentation +[group('docs')] +docs-build: + uv run mkdocs build --clean - if {{pecos}} go check -q >/dev/null 2>&1; then - echo "Go detected, running Go tests..." - {{pecos}} go test - else - echo "" - echo "WARNING: Go is not installed. Skipping Go tests." - echo " To run Go tests, please install Go from https://go.dev/dl/" - echo "" - fi +# Test Python code examples in documentation +[group('docs')] +docs-test: + uv run python scripts/docs/generate_doc_tests.py + uv run pytest python/quantum-pecos/tests/docs/generated -v -k "not rust" -m "not slow" # ============================================================================= -# Decoders +# Deps Management (prefer `just setup` or `pecos install `) # ============================================================================= -# Build all decoder crates with all features -build-decoders: - cargo build --package pecos-decoders --all-features +# Install LLVM 14 +[group('deps')] +install-llvm: check-cli + {{pecos}} install llvm -# Build specific decoder (e.g., just build-decoder ldpc) -build-decoder decoder: - cargo build --package pecos-decoders --features {{decoder}} +# Install CUDA Toolkit +[group('deps')] +install-cuda: check-cli + {{pecos}} install cuda -# Test all decoder crates -test-decoders: - cargo test --package pecos-decoders --all-features +# Configure LLVM paths in .cargo/config.toml +[group('deps')] +configure-llvm: check-cli + {{pecos}} llvm configure -# Test specific decoder -test-decoder decoder: - cargo test --package pecos-decoders --features {{decoder}} +# Check LLVM 14 installation status +[group('deps')] +check-llvm: check-cli + -{{pecos}} llvm check -# Show available decoders and their features -decoder-info: - @echo "Available decoders in PECOS:" - @echo " - ldpc: LDPC decoders (BP-OSD, MBP, etc.)" - @echo "" - @echo "To build specific decoder: just build-decoder ldpc" - @echo "To build all decoders: just build-decoders" - @echo "See DECODERS.md for detailed documentation." - -# Show decoder download cache status -decoder-cache-status: - {{pecos}} list -v - -# Clean decoder download cache (same as clean-cache) -decoder-cache-clean: clean-cache - @echo "Decoder cache cleaned (part of ~/.pecos/cache/)" +# Check CUDA installation status +[group('deps')] +check-cuda: check-cli + -{{pecos}} cuda check # ============================================================================= # Julia Bindings # ============================================================================= # Build Julia FFI library -julia-build profile="release": +[group('julia')] +julia-build profile="release": check-cli {{pecos}} julia build --profile {{profile}} -# Build Julia FFI library in debug mode -julia-build-debug: - {{pecos}} julia build --profile debug - -# Run Julia tests (requires Julia installed) -julia-test: +# Run Julia tests +[group('julia')] +julia-test: check-cli {{pecos}} julia test -# Run Julia examples -julia-examples: julia-build-debug - #!/usr/bin/env bash - set -euo pipefail - echo "Running Julia examples..." - if {{pecos}} julia check -q >/dev/null 2>&1; then - cd julia/PECOS.jl && julia --project=. examples/demo.jl - cd julia/PECOS.jl && julia --project=. examples/basic_usage.jl - else - echo "Julia not found. Please install Julia to run examples." - exit 1 - fi - -# Show Julia package information -julia-info: - @echo "Julia Package Information:" - @echo "=========================" - @echo "Package name: PECOS.jl" - @echo "Location: julia/PECOS.jl" - @echo "FFI library: julia/pecos-julia-ffi" - @echo "" - @echo "To install for development:" - @echo " 1. Build FFI library: pecos julia build" - @echo " 2. In Julia REPL: ] add julia/PECOS.jl" - @echo "" - @echo "To run tests: pecos julia test" - @echo "To run examples: just julia-examples" - # Format Julia code -julia-format: +[group('julia')] +julia-format: check-cli {{pecos}} julia fmt # Check Julia code formatting -julia-format-check: +[group('julia')] +julia-format-check: check-cli {{pecos}} julia fmt --check -# Run Aqua.jl quality checks on Julia code -julia-lint: +# Run Aqua.jl quality checks +[group('julia')] +julia-lint: check-cli {{pecos}} julia lint -# Clean Julia build artifacts -julia-clean: - @echo "Cleaning Julia artifacts..." - rm -f julia/PECOS.jl/Manifest.toml || true - rm -f julia/PECOS.jl/dev/PECOS_julia_jll/Manifest.toml || true - find julia -name "*.jl.*.cov" -delete 2>/dev/null || true - find julia -name "*.jl.cov" -delete 2>/dev/null || true - find julia -name "*.jl.mem" -delete 2>/dev/null || true - # ============================================================================= # Go Bindings # ============================================================================= # Build Go FFI library -go-build profile="release": +[group('go')] +go-build profile="release": check-cli {{pecos}} go build --profile {{profile}} -# Build Go FFI library in debug mode -go-build-debug: - {{pecos}} go build --profile debug - -# Run Go tests (requires Go installed) -go-test: +# Run Go tests +[group('go')] +go-test: check-cli {{pecos}} go test -# Show Go package information -go-info: - @echo "Go Package Information:" - @echo "=======================" - @echo "Package name: github.com/PECOS-packages/PECOS/go/pecos" - @echo "Location: go/pecos" - @echo "FFI library: go/pecos-go-ffi" - @echo "" - @echo "To build and test:" - @echo " 1. Build FFI library: pecos go build" - @echo " 2. Run tests: pecos go test" - @echo "" - @echo "To use in your Go project:" - @echo " 1. Set LD_LIBRARY_PATH to include target/release" - @echo " 2. Import: github.com/PECOS-packages/PECOS/go/pecos" - # Format Go code +[group('go')] go-fmt: - {{pecos}} go fmt + gofmt -w go/pecos # Check Go code formatting +[group('go')] go-fmt-check: - {{pecos}} go fmt --check + @test -z "$(gofmt -l go/pecos)" || (gofmt -l go/pecos && exit 1) # Run Go linting with go vet +[group('go')] go-lint: - {{pecos}} go lint + cd go/pecos && go vet ./... -# Clean Go build artifacts -go-clean: +# ============================================================================= +# Decoders +# ============================================================================= + +# Build all decoder crates +[group('decoders')] +build-decoders: + cargo build --package pecos-decoders --all-features + +# Test all decoder crates +[group('decoders')] +test-decoders: + cargo test --package pecos-decoders --all-features + +# Build specific decoder (e.g., just build-decoder ldpc) +[group('decoders')] +build-decoder decoder: + cargo build --package pecos-decoders --features {{decoder}} + +# Test specific decoder +[group('decoders')] +test-decoder decoder: + cargo test --package pecos-decoders --features {{decoder}} + +# ============================================================================= +# Additional Testing +# ============================================================================= + +# Run Rust tests in default cargo profile +[private] +rstest-all: check-cli + {{pecos}} rust test + +# Run NumPy/SciPy compatibility tests +[group('test')] +pytest-numpy: + uv run --group numpy-compat pytest python/pecos-rslib/tests -m "numpy and not performance" + +# Run performance tests with release build +[group('test')] +pytest-perf: build-release + uv run --group numpy-compat pytest python/pecos-rslib/tests -m "performance" -v + +# Run tests for optional dependencies +[group('test')] +pytest-dep: + uv run pytest python/pecos-rslib/tests -m "optional_dependency" + uv run pytest python/quantum-pecos/tests -m "optional_dependency" + +# Run Selene plugin tests +[group('test')] +pytest-selene: + uv run pytest python/selene-plugins + +# Run all tests with warnings for missing tools +[group('test')] +test-all: rstest-all pytest-all #!/usr/bin/env bash set -euo pipefail - echo "Cleaning Go artifacts..." - rm -f go/pecos/go.sum || true - if {{pecos}} go check -q >/dev/null 2>&1; then - cd go/pecos && go clean -cache 2>/dev/null || true + if command -v julia >/dev/null 2>&1; then + echo "Julia detected, running Julia tests..." + {{pecos}} julia test + else + echo "" + echo "WARNING: Julia is not installed. Skipping Julia tests." + echo " To run Julia tests, please install Julia from https://julialang.org/downloads/" + echo "" + fi + if command -v go >/dev/null 2>&1; then + echo "Go detected, running Go tests..." + {{pecos}} go test + else + echo "" + echo "WARNING: Go is not installed. Skipping Go tests." + echo " To run Go tests, please install Go from https://go.dev/dl/" + echo "" fi # ============================================================================= -# Cleaning (Cross-platform via Python script) +# Cleaning # ============================================================================= -# Clean build artifacts (cross-platform) -clean: - uv run python scripts/clean.py - # Clean Selene plugin build artifacts +[group('clean')] clean-selene: uv run python scripts/clean.py --selene # Clean ~/.pecos/cache/ and ~/.pecos/tmp/ +[group('clean')] clean-cache: uv run python scripts/clean.py --cache # Clean ~/.pecos/deps/ (extracted C++ dependencies) +[group('clean')] clean-deps: uv run python scripts/clean.py --deps -# Clean ~/.pecos/llvm/ (WARNING: slow to reinstall) -clean-llvm: - uv run python scripts/clean.py --llvm - -# Clean ~/.pecos/cuda/ (WARNING: slow to reinstall) -clean-cuda: - uv run python scripts/clean.py --cuda - -# Clean ~/.pecos/ except LLVM and CUDA -clean-pecos-home: - uv run python scripts/clean.py --cache --deps - -# Clean project artifacts + ~/.pecos/ (except LLVM/CUDA) -clean-all: - uv run python scripts/clean.py --cache --deps - -# Nuclear option: clean everything including LLVM and CUDA +# Clean everything including LLVM and CUDA +[group('clean')] clean-everything: uv run python scripts/clean.py --all -# Preview what would be cleaned (dry run) +# Preview what would be cleaned +[group('clean')] clean-dry-run: uv run python scripts/clean.py --dry-run # ============================================================================= -# Development Workflows +# Private / Internal Recipes # ============================================================================= -# Verify LLVM configuration before building -pre-check: check-cli - {{pecos}} llvm check +[private] +check-cli: + #!/usr/bin/env bash + if ! command -v pecos >/dev/null 2>&1; then + echo "Error: PECOS CLI not found. Install with: just install-cli" + exit 1 + fi + expected=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') + installed=$(pecos --version 2>/dev/null | awk '{print $2}') + if [[ "$installed" != "$expected" ]]; then + echo "Warning: PECOS CLI outdated (installed: ${installed:-unknown}, expected: $expected)" + echo " Update with: just reinstall-cli" + fi -# Dev cycle: incremental build + test (fast, for normal development) -dev cuda="false": pre-check (build-dev cuda) test +[private] +setup-quiet: check-cli + {{pecos}} setup --quiet -# Dev cycle with CUDA support -devc: (dev "true") +[private] +installreqs: + #!/usr/bin/env bash + set -euo pipefail + echo "Installing requirements..." + if python -c "import cupy" >/dev/null 2>&1; then + echo "(including CUDA packages)" + uv sync --project . --all-packages --group cuda + else + uv sync --project . --all-packages + fi -# Full dev cycle: clean build + test + lint (pre-merge) -dev-full cuda="false": pre-check clean (build-dev cuda) test lint +[private] +build-selene: + #!/usr/bin/env bash + set -euo pipefail + echo "Building Selene plugins..." + CARGO_ARGS="" + for DIR in python/selene-plugins/pecos-selene-*/; do + CARGO_ARGS="$CARGO_ARGS -p $(basename "$DIR")" + done + if [ -n "$CARGO_ARGS" ]; then + cargo build --release $CARGO_ARGS + fi + echo "Copying libraries to Python packages..." + {{pecos}} selene install --profile release + echo "Selene plugins built and installed successfully" -# Full dev cycle with CUDA support -devc-full: (dev-full "true") +[private] +pre-check: check-cli setup-quiet -# Internal: build for dev cycle with optional CUDA [private] build-dev cuda="false": installreqs build-selene #!/usr/bin/env bash @@ -650,21 +506,149 @@ build-dev cuda="false": installreqs build-selene else {{pecos}} python build --profile debug fi - # Build FFI crates if tools available {{pecos}} julia build --profile debug 2>/dev/null || true {{pecos}} go build --profile debug 2>/dev/null || true -# Install uv using pip (prefer: https://docs.astral.sh/uv/getting-started/installation/) +# Convenience aliases +[private] +build-debug: (build "debug") +[private] +build-release: (build "release") +[private] +build-native: (build "native") +[private] +build-lite-debug: (build-lite "debug") +[private] +build-lite-release: (build-lite "release") +[private] +build-cuda-debug: (build-cuda "debug") +[private] +build-cuda-release: (build-cuda "release") +[private] +build-cuda-native: (build-cuda "native") + +# Remaining utility recipes + +# Generate/update lockfiles +[group('setup')] +updatereqs: + uv self update + uv lock --project . + +# Install requirements with specific Python version +[private] +installreqs-python version: + #!/usr/bin/env bash + set -euo pipefail + if python -c "import cupy" >/dev/null 2>&1; then + uv sync --project . --all-packages --python "{{version}}" --group cuda + else + uv sync --project . --all-packages --python "{{version}}" + fi + +# Install uv using pip +[group('setup')] pip-install-uv: - @echo "Installing uv..." python -m pip install --upgrade uv - @echo "Creating venv and installing dependencies..." uv sync -# Show system information -sys-info: - {{pecos}} sys-info +# Normalize line endings according to .gitattributes +[private] +normalize-line-endings: + -git rm --cached -r . + git reset --hard -# List installed and cached dependencies -list-deps: +# Install CUDA Python packages (requires CUDA toolkit) +[private] +install-cuda-python: + {{pecos}} cuda setup-python + +# Full CUDA setup: toolkit + Python packages +[private] +install-cuda-full: install-cuda install-cuda-python + +# Validate CUDA installation integrity +[private] +validate-cuda: + {{pecos}} cuda validate + +# Docs extras +[private] +docs-test-slow: + uv run python scripts/docs/generate_doc_tests.py + uv run pytest python/quantum-pecos/tests/docs/generated -v -k "not rust" + +[private] +docs-test-generate: + uv run python scripts/docs/generate_doc_tests.py + +[private] +docs-test-run *args: + uv run pytest python/quantum-pecos/tests/docs/generated {{args}} + +[private] +docs-test-legacy: + uv run python scripts/docs/test_code_examples.py + +# Julia/Go extras +[private] +julia-build-debug: + {{pecos}} julia build --profile debug + +[private] +julia-examples: julia-build-debug + #!/usr/bin/env bash + set -euo pipefail + if command -v julia >/dev/null 2>&1; then + cd julia/PECOS.jl && julia --project=. examples/demo.jl + cd julia/PECOS.jl && julia --project=. examples/basic_usage.jl + else + echo "Julia not found."; exit 1 + fi + +[private] +julia-info: + @echo "Julia: julia/PECOS.jl | FFI: julia/pecos-julia-ffi" + +[private] +julia-clean: + rm -f julia/PECOS.jl/Manifest.toml julia/PECOS.jl/dev/PECOS_julia_jll/Manifest.toml || true + +[private] +go-build-debug: + {{pecos}} go build --profile debug + +[private] +go-info: + @echo "Go: go/pecos | FFI: go/pecos-go-ffi" + +[private] +go-clean: + rm -f go/pecos/go.sum || true + +[private] +decoder-info: + @echo "Decoders: ldpc (BP-OSD, MBP). See DECODERS.md" + +[private] +decoder-cache-status: {{pecos}} list -v + +[private] +decoder-cache-clean: clean-cache + +[private] +clean-llvm: + uv run python scripts/clean.py --llvm + +[private] +clean-cuda: + uv run python scripts/clean.py --cuda + +[private] +clean-pecos-home: + uv run python scripts/clean.py --cache --deps + +[private] +clean-all: + uv run python scripts/clean.py --cache --deps diff --git a/crates/pecos-build/src/cuda.rs b/crates/pecos-build/src/cuda.rs index 5d6e0b619..ec9aa24ca 100644 --- a/crates/pecos-build/src/cuda.rs +++ b/crates/pecos-build/src/cuda.rs @@ -1,7 +1,7 @@ //! CUDA Toolkit management for PECOS //! //! This module provides functionality to download, install, and manage -//! CUDA Toolkit installations in `~/.pecos/cuda/`. +//! CUDA Toolkit installations in `~/.pecos/deps/cuda/`. pub mod installer; @@ -13,26 +13,43 @@ use crate::errors::{Error, Result}; /// CUDA version we install pub const CUDA_VERSION: &str = "12.6.3"; -/// Get the pecos CUDA installation directory -#[must_use] -pub fn get_pecos_cuda_dir() -> Option { +/// Get the pecos CUDA installation directory (`~/.pecos/deps/cuda/`) +/// +/// # Errors +/// +/// Returns an error if unable to determine the path +pub fn get_pecos_cuda_dir() -> Result { + crate::home::get_cuda_dir_path() +} + +/// Get the legacy CUDA installation directory (`~/.pecos/cuda/`) +fn get_legacy_cuda_dir() -> Option { dirs::home_dir().map(|h| h.join(".pecos").join("cuda")) } /// Find CUDA installation, checking local first, then system /// /// Search order: -/// 1. `~/.pecos/cuda/` (local installation) -/// 2. `CUDA_PATH` environment variable -/// 3. `nvcc` in PATH (derive `CUDA_PATH` from nvcc location) -/// 4. Standard system paths (`/usr/local/cuda`, etc.) +/// 1. `~/.pecos/deps/cuda/` (new local installation) +/// 2. `~/.pecos/cuda/` (legacy path, prints deprecation warning) +/// 3. `CUDA_PATH` environment variable +/// 4. `nvcc` in PATH (derive `CUDA_PATH` from nvcc location) +/// 5. Standard system paths (`/usr/local/cuda`, etc.) #[must_use] pub fn find_cuda() -> Option { - // 1. Check ~/.pecos/cuda/ first (local installation) - if let Some(pecos_cuda) = get_pecos_cuda_dir() - && is_valid_cuda_installation(&pecos_cuda) + // 1. Check new deps path: ~/.pecos/deps/cuda/ + if let Ok(deps_cuda) = get_pecos_cuda_dir() + && is_valid_cuda_installation(&deps_cuda) + { + return Some(deps_cuda); + } + + // 2. Check legacy top-level path: ~/.pecos/cuda/ + if let Some(legacy_cuda) = get_legacy_cuda_dir() + && is_valid_cuda_installation(&legacy_cuda) { - return Some(pecos_cuda); + crate::home::print_legacy_warning("CUDA", &legacy_cuda); + return Some(legacy_cuda); } // 2. Check CUDA_PATH environment variable diff --git a/crates/pecos-build/src/cuda/installer.rs b/crates/pecos-build/src/cuda/installer.rs index e6b8a279a..6109774f6 100644 --- a/crates/pecos-build/src/cuda/installer.rs +++ b/crates/pecos-build/src/cuda/installer.rs @@ -1,6 +1,6 @@ //! CUDA Toolkit installation functionality //! -//! Downloads and installs CUDA Toolkit to `~/.pecos/cuda/` +//! Downloads and installs CUDA Toolkit to `~/.pecos/deps/cuda/` #![allow(clippy::case_sensitive_file_extension_comparisons)] @@ -56,7 +56,7 @@ fn get_download_info() -> Result { } } -/// Install CUDA Toolkit to `~/.pecos/cuda/` +/// Install CUDA Toolkit to `~/.pecos/deps/cuda/` /// /// # Arguments /// * `force` - Force reinstall even if already present @@ -69,8 +69,7 @@ fn get_download_info() -> Result { /// - Download or extraction fails /// - Installation verification fails pub fn install_cuda(force: bool) -> Result { - let cuda_dir = get_pecos_cuda_dir() - .ok_or_else(|| Error::HomeDir("Could not determine home directory".into()))?; + let cuda_dir = get_pecos_cuda_dir()?; // Check if already installed if !force && cuda_dir.exists() && is_valid_cuda_installation(&cuda_dir) { @@ -407,18 +406,17 @@ fn extract_windows_exe(archive: &Path, dest: &Path) -> Result<()> { Ok(()) } -/// Uninstall CUDA from ~/.pecos/cuda/ +/// Uninstall CUDA from `~/.pecos/deps/cuda/` /// /// # Errors /// Returns an error if: /// - Home directory cannot be determined /// - Directory removal fails pub fn uninstall_cuda() -> Result<()> { - let cuda_dir = get_pecos_cuda_dir() - .ok_or_else(|| Error::HomeDir("Could not determine home directory".into()))?; + let cuda_dir = get_pecos_cuda_dir()?; if !cuda_dir.exists() { - println!("CUDA is not installed in ~/.pecos/cuda/"); + println!("CUDA is not installed in ~/.pecos/deps/cuda/"); return Ok(()); } diff --git a/crates/pecos-build/src/cuquantum.rs b/crates/pecos-build/src/cuquantum.rs index 5583e573a..ab50d7ab3 100644 --- a/crates/pecos-build/src/cuquantum.rs +++ b/crates/pecos-build/src/cuquantum.rs @@ -1,7 +1,7 @@ //! cuQuantum SDK management for PECOS //! //! This module provides functionality to detect, download, and manage -//! cuQuantum SDK installations in `~/.pecos/cuquantum/`. +//! cuQuantum SDK installations in `~/.pecos/deps/cuquantum/`. //! //! cuQuantum is NVIDIA's SDK for accelerated quantum circuit simulation, //! including cuStateVec (state vector) and cuTensorNet (tensor network). @@ -21,26 +21,43 @@ use crate::errors::{Error, Result}; /// Note: CUDA 13 support requires version 25.09.0.7 or later pub const CUQUANTUM_VERSION: &str = "25.11.1.11"; -/// Get the pecos cuQuantum installation directory -#[must_use] -pub fn get_pecos_cuquantum_dir() -> Option { +/// Get the pecos cuQuantum installation directory (`~/.pecos/deps/cuquantum/`) +/// +/// # Errors +/// +/// Returns an error if unable to determine the path +pub fn get_pecos_cuquantum_dir() -> Result { + crate::home::get_cuquantum_dir_path() +} + +/// Get the legacy cuQuantum installation directory (`~/.pecos/cuquantum/`) +fn get_legacy_cuquantum_dir() -> Option { dirs::home_dir().map(|h| h.join(".pecos").join("cuquantum")) } /// Find cuQuantum installation, checking local first, then system /// /// Search order: -/// 1. `~/.pecos/cuquantum/` (local installation) -/// 2. `CUQUANTUM_ROOT` environment variable -/// 3. Standard system paths (`/usr/local/cuquantum`, etc.) -/// 4. Derive from CUDA installation path +/// 1. `~/.pecos/deps/cuquantum/` (new local installation) +/// 2. `~/.pecos/cuquantum/` (legacy path, prints deprecation warning) +/// 3. `CUQUANTUM_ROOT` environment variable +/// 4. Standard system paths (`/usr/local/cuquantum`, etc.) +/// 5. Derive from CUDA installation path #[must_use] pub fn find_cuquantum() -> Option { - // 1. Check ~/.pecos/cuquantum/ first (local installation) - if let Some(pecos_cuquantum) = get_pecos_cuquantum_dir() - && is_valid_cuquantum_installation(&pecos_cuquantum) + // 1. Check new deps path: ~/.pecos/deps/cuquantum/ + if let Ok(deps_cuquantum) = get_pecos_cuquantum_dir() + && is_valid_cuquantum_installation(&deps_cuquantum) + { + return Some(deps_cuquantum); + } + + // 2. Check legacy top-level path: ~/.pecos/cuquantum/ + if let Some(legacy_cuquantum) = get_legacy_cuquantum_dir() + && is_valid_cuquantum_installation(&legacy_cuquantum) { - return Some(pecos_cuquantum); + crate::home::print_legacy_warning("cuQuantum", &legacy_cuquantum); + return Some(legacy_cuquantum); } // 2. Check CUQUANTUM_ROOT environment variable diff --git a/crates/pecos-build/src/cuquantum/config.rs b/crates/pecos-build/src/cuquantum/config.rs index 1e5d7e5b5..ec0c2cda9 100644 --- a/crates/pecos-build/src/cuquantum/config.rs +++ b/crates/pecos-build/src/cuquantum/config.rs @@ -122,8 +122,8 @@ pub fn validate_cuquantum_config() -> ConfigValidation { /// /// Returns an error if no suitable cuQuantum installation could be found pub fn auto_configure_cuquantum(project_root: Option) -> Result { - // Priority 1: Check ~/.pecos/cuquantum for PECOS-managed cuQuantum - if let Some(pecos_cuquantum) = get_pecos_cuquantum_dir() + // Priority 1: Check ~/.pecos/deps/cuquantum for PECOS-managed cuQuantum + if let Ok(pecos_cuquantum) = get_pecos_cuquantum_dir() && is_valid_cuquantum_installation(&pecos_cuquantum) { let project_root = project_root diff --git a/crates/pecos-build/src/cuquantum/installer.rs b/crates/pecos-build/src/cuquantum/installer.rs index b4386a81f..ba4118564 100644 --- a/crates/pecos-build/src/cuquantum/installer.rs +++ b/crates/pecos-build/src/cuquantum/installer.rs @@ -1,6 +1,6 @@ //! cuQuantum SDK installation functionality //! -//! Downloads and installs cuQuantum SDK to `~/.pecos/cuquantum/` +//! Downloads and installs cuQuantum SDK to `~/.pecos/deps/cuquantum/` use crate::errors::{Error, Result}; use sha2::{Digest, Sha256}; @@ -99,8 +99,7 @@ fn get_download_info() -> Result { /// - Download or extraction fails /// - Installation verification fails pub fn install_cuquantum(force: bool) -> Result { - let cuquantum_dir = get_pecos_cuquantum_dir() - .ok_or_else(|| Error::HomeDir("Could not determine home directory".into()))?; + let cuquantum_dir = get_pecos_cuquantum_dir()?; // Check if already installed if !force && cuquantum_dir.exists() && is_valid_cuquantum_installation(&cuquantum_dir) { @@ -388,18 +387,17 @@ fn extract_zip(archive: &Path, dest: &Path) -> Result<()> { Ok(()) } -/// Uninstall cuQuantum from ~/.pecos/cuquantum/ +/// Uninstall cuQuantum from `~/.pecos/deps/cuquantum/` /// /// # Errors /// Returns an error if: /// - Home directory cannot be determined /// - Directory removal fails pub fn uninstall_cuquantum() -> Result<()> { - let cuquantum_dir = get_pecos_cuquantum_dir() - .ok_or_else(|| Error::HomeDir("Could not determine home directory".into()))?; + let cuquantum_dir = get_pecos_cuquantum_dir()?; if !cuquantum_dir.exists() { - println!("cuQuantum is not installed in ~/.pecos/cuquantum/"); + println!("cuQuantum is not installed in ~/.pecos/deps/cuquantum/"); return Ok(()); } diff --git a/crates/pecos-build/src/home.rs b/crates/pecos-build/src/home.rs index 17443058d..1c233951c 100644 --- a/crates/pecos-build/src/home.rs +++ b/crates/pecos-build/src/home.rs @@ -5,11 +5,22 @@ //! ```text //! ~/.pecos/ //! ├── cache/ # Downloaded archives (tar.gz, 7z, etc.) -//! ├── deps/ # Extracted & patched sources (ready to build) -//! ├── llvm/ # LLVM installation +//! ├── deps/ # All dependencies (LLVM, CUDA, cuQuantum, C++ libs, etc.) +//! │ ├── llvm/ +//! │ ├── cuda/ +//! │ ├── cuquantum/ +//! │ ├── quest-v4.1.0/ +//! │ └── ... //! └── tmp/ # Temporary files during downloads/extraction //! ``` //! +//! # Legacy paths +//! +//! Earlier versions installed LLVM, CUDA, and cuQuantum at the top level +//! (`~/.pecos/llvm/`, `~/.pecos/cuda/`, `~/.pecos/cuquantum/`). Detection +//! still checks these paths as a fallback, but new installs go under `deps/`. +//! Run `pecos migrate` to move legacy installs into `deps/`. +//! //! # Environment Variables //! //! - `PECOS_HOME`: Override the entire home directory (default: `~/.pecos/`) @@ -107,18 +118,18 @@ pub fn get_deps_dir() -> Result { /// Get the LLVM installation directory path (without creating it) /// -/// Returns `$PECOS_HOME/llvm/` +/// Returns `$PECOS_HOME/deps/llvm/` /// /// # Errors /// /// Returns an error if unable to determine the path pub fn get_llvm_dir_path() -> Result { - Ok(get_pecos_home_path()?.join("llvm")) + Ok(get_deps_dir_path()?.join("llvm")) } /// Get the LLVM installation directory (creates if needed) /// -/// Returns `$PECOS_HOME/llvm/` +/// Returns `$PECOS_HOME/deps/llvm/` /// /// # Errors /// @@ -129,6 +140,54 @@ pub fn get_llvm_dir() -> Result { Ok(llvm_dir) } +/// Get the CUDA installation directory path (without creating it) +/// +/// Returns `$PECOS_HOME/deps/cuda/` +/// +/// # Errors +/// +/// Returns an error if unable to determine the path +pub fn get_cuda_dir_path() -> Result { + Ok(get_deps_dir_path()?.join("cuda")) +} + +/// Get the CUDA installation directory (creates if needed) +/// +/// Returns `$PECOS_HOME/deps/cuda/` +/// +/// # Errors +/// +/// Returns an error if unable to determine or create the CUDA directory +pub fn get_cuda_dir() -> Result { + let cuda_dir = get_cuda_dir_path()?; + fs::create_dir_all(&cuda_dir)?; + Ok(cuda_dir) +} + +/// Get the cuQuantum installation directory path (without creating it) +/// +/// Returns `$PECOS_HOME/deps/cuquantum/` +/// +/// # Errors +/// +/// Returns an error if unable to determine the path +pub fn get_cuquantum_dir_path() -> Result { + Ok(get_deps_dir_path()?.join("cuquantum")) +} + +/// Get the cuQuantum installation directory (creates if needed) +/// +/// Returns `$PECOS_HOME/deps/cuquantum/` +/// +/// # Errors +/// +/// Returns an error if unable to determine or create the cuQuantum directory +pub fn get_cuquantum_dir() -> Result { + let dir = get_cuquantum_dir_path()?; + fs::create_dir_all(&dir)?; + Ok(dir) +} + /// Get the cache directory path (without creating it) /// /// Returns `$PECOS_CACHE_DIR` if set, otherwise `$PECOS_HOME/cache/` @@ -187,6 +246,100 @@ pub fn get_tmp_dir() -> Result { Ok(tmp_dir) } +// ── Legacy path helpers ───────────────────────────────────────────────────── +// +// Earlier versions installed LLVM/CUDA/cuQuantum at the top level of +// ~/.pecos/. These helpers detect the old locations so that detection code +// can fall back gracefully and `pecos migrate` can move them. + +/// Legacy LLVM path: `~/.pecos/llvm/` +/// +/// # Errors +/// +/// Returns an error if unable to determine the path +pub fn get_legacy_llvm_dir_path() -> Result { + Ok(get_pecos_home_path()?.join("llvm")) +} + +/// Legacy CUDA path: `~/.pecos/cuda/` +/// +/// # Errors +/// +/// Returns an error if unable to determine the path +pub fn get_legacy_cuda_dir_path() -> Result { + Ok(get_pecos_home_path()?.join("cuda")) +} + +/// Legacy cuQuantum path: `~/.pecos/cuquantum/` +/// +/// # Errors +/// +/// Returns an error if unable to determine the path +pub fn get_legacy_cuquantum_dir_path() -> Result { + Ok(get_pecos_home_path()?.join("cuquantum")) +} + +/// Print a deprecation warning for a legacy top-level install path. +pub fn print_legacy_warning(name: &str, old_path: &Path) { + eprintln!( + "Warning: {name} found at legacy path: {}", + old_path.display() + ); + eprintln!(" Run `pecos migrate` to move it to ~/.pecos/deps/"); +} + +/// Description of a single legacy dep that can be migrated. +pub struct LegacyDep { + /// Human-readable name (e.g. "LLVM 14") + pub name: &'static str, + /// Old path + pub old: PathBuf, + /// New path + pub new: PathBuf, +} + +/// Check for legacy top-level installs that should be migrated. +/// +/// Returns a list of deps whose old path exists but new path does not. +/// +/// # Errors +/// +/// Returns an error if unable to determine paths. +pub fn find_legacy_deps() -> Result> { + let mut found = Vec::new(); + let checks: [(&str, Result, Result); 3] = [ + ("LLVM", get_legacy_llvm_dir_path(), get_llvm_dir_path()), + ("CUDA", get_legacy_cuda_dir_path(), get_cuda_dir_path()), + ( + "cuQuantum", + get_legacy_cuquantum_dir_path(), + get_cuquantum_dir_path(), + ), + ]; + for (name, old_result, new_result) in checks { + let (Ok(old), Ok(new)) = (old_result, new_result) else { + continue; + }; + if old.exists() && !new.exists() { + found.push(LegacyDep { name, old, new }); + } + } + Ok(found) +} + +/// Migrate a single legacy dep by renaming old -> new. +/// +/// # Errors +/// +/// Returns an error if the rename fails. +pub fn migrate_legacy_dep(dep: &LegacyDep) -> Result<()> { + if let Some(parent) = dep.new.parent() { + fs::create_dir_all(parent)?; + } + fs::rename(&dep.old, &dep.new)?; + Ok(()) +} + /// Get information about the PECOS home directory #[derive(Debug)] pub struct HomeInfo { @@ -196,6 +349,10 @@ pub struct HomeInfo { pub deps: PathBuf, /// Path to LLVM directory pub llvm: PathBuf, + /// Path to CUDA directory + pub cuda: PathBuf, + /// Path to cuQuantum directory + pub cuquantum: PathBuf, /// Path to cache directory pub cache: PathBuf, /// Path to tmp directory @@ -218,6 +375,8 @@ pub fn get_home_info() -> Result { home: get_pecos_home()?, deps: get_deps_dir()?, llvm: get_llvm_dir()?, + cuda: get_cuda_dir()?, + cuquantum: get_cuquantum_dir()?, cache: get_cache_dir()?, tmp: get_tmp_dir()?, home_overridden: std::env::var("PECOS_HOME").is_ok(), diff --git a/crates/pecos-build/src/lib.rs b/crates/pecos-build/src/lib.rs index 17d2de446..33b4bad41 100644 --- a/crates/pecos-build/src/lib.rs +++ b/crates/pecos-build/src/lib.rs @@ -63,13 +63,17 @@ pub mod extract; pub mod home; pub mod llvm; pub mod manifest; +pub mod prompt; // Re-export main types for convenience pub use deps::ensure_dep_ready; pub use download::{DownloadInfo, download_all_cached, download_cached}; pub use errors::{Error, Result}; pub use extract::{extract_archive, extract_to_deps}; -pub use home::{get_cache_dir, get_deps_dir, get_llvm_dir, get_pecos_home, get_tmp_dir}; +pub use home::{ + get_cache_dir, get_cuda_dir, get_cuquantum_dir, get_deps_dir, get_llvm_dir, get_pecos_home, + get_tmp_dir, +}; pub use manifest::Manifest; /// Check that the C++ toolchain supports C++20 with the CXX crate. diff --git a/crates/pecos-build/src/llvm.rs b/crates/pecos-build/src/llvm.rs index d975f32da..572476012 100644 --- a/crates/pecos-build/src/llvm.rs +++ b/crates/pecos-build/src/llvm.rs @@ -36,18 +36,25 @@ pub const REQUIRED_VERSION: &str = "14"; /// Find LLVM 14 installation on the system. /// /// This function searches for LLVM 14 in the following priority order: -/// 1. Home directory: -/// - Windows: `~/.pecos/LLVM-14` (new) or `~/.pecos/llvm` (legacy) -/// - Unix: `~/.pecos/llvm` -/// 2. Project-local installation (`llvm/` directory relative to repository root) -/// 3. System installations (platform-specific locations) +/// 1. PECOS deps directory: `~/.pecos/deps/llvm/` +/// 2. Legacy PECOS path: `~/.pecos/llvm/` (prints deprecation warning) +/// - Windows also checks: `~/.pecos/LLVM-14` +/// 3. Project-local installation (`llvm/` directory relative to repository root) +/// 4. System installations (platform-specific locations) /// /// # Returns /// - `Some(PathBuf)` if LLVM 14 is found and valid /// - `None` if LLVM 14 is not found #[must_use] pub fn find_llvm_14(repo_root: Option) -> Option { - // 1. Check home directory + // 1. Check new deps path: ~/.pecos/deps/llvm/ + if let Ok(deps_llvm) = crate::home::get_llvm_dir_path() + && is_valid_llvm_14(&deps_llvm) + { + return Some(deps_llvm); + } + + // 2. Check legacy top-level path: ~/.pecos/llvm/ if let Some(home_dir) = dirs::home_dir() { let pecos_dir = home_dir.join(".pecos"); @@ -55,24 +62,19 @@ pub fn find_llvm_14(repo_root: Option) -> Option { { let user_llvm_new = pecos_dir.join("LLVM-14"); if is_valid_llvm_14(&user_llvm_new) { + crate::home::print_legacy_warning("LLVM", &user_llvm_new); return Some(user_llvm_new); } - let user_llvm_legacy = pecos_dir.join("llvm"); - if is_valid_llvm_14(&user_llvm_legacy) { - return Some(user_llvm_legacy); - } } - #[cfg(not(target_os = "windows"))] - { - let user_llvm = pecos_dir.join("llvm"); - if is_valid_llvm_14(&user_llvm) { - return Some(user_llvm); - } + let user_llvm_legacy = pecos_dir.join("llvm"); + if is_valid_llvm_14(&user_llvm_legacy) { + crate::home::print_legacy_warning("LLVM", &user_llvm_legacy); + return Some(user_llvm_legacy); } } - // 2. Check for project-local LLVM + // 3. Check for project-local LLVM if let Some(root) = repo_root { let local_llvm = root.join("llvm"); if is_valid_llvm_14(&local_llvm) { @@ -80,7 +82,7 @@ pub fn find_llvm_14(repo_root: Option) -> Option { } } - // 3. Check system installations + // 4. Check system installations find_system_llvm_14() } diff --git a/crates/pecos-build/src/llvm/config.rs b/crates/pecos-build/src/llvm/config.rs index e3693d2a7..926bda5f5 100644 --- a/crates/pecos-build/src/llvm/config.rs +++ b/crates/pecos-build/src/llvm/config.rs @@ -92,50 +92,30 @@ impl ConfigValidation { } /// Read the configured LLVM path from .cargo/config.toml +/// +/// Handles both TOML formats: +/// `LLVM_SYS_140_PREFIX = "/path/to/llvm"` +/// `LLVM_SYS_140_PREFIX = { value = "/path/to/llvm", force = true }` #[must_use] -#[allow(clippy::collapsible_if)] pub fn read_configured_llvm_path() -> Option { let project_root = find_cargo_project_root()?; let config_path = project_root.join(".cargo").join("config.toml"); - let content = fs::read_to_string(&config_path).ok()?; + let table: toml::Table = content.parse().ok()?; - // Parse out LLVM_SYS_140_PREFIX value - // Handles both formats: - // LLVM_SYS_140_PREFIX = "/path/to/llvm" - // LLVM_SYS_140_PREFIX = { value = "/path/to/llvm", force = true } - for line in content.lines() { - let trimmed = line.trim(); - if trimmed.starts_with("LLVM_SYS_140_PREFIX") { - if let Some(eq_pos) = trimmed.find('=') { - let value_part = trimmed[eq_pos + 1..].trim(); - - // Check for inline table format: { value = "...", ... } - if value_part.starts_with('{') { - if let Some(value_start) = value_part.find("value") { - let after_value = &value_part[value_start + 5..]; - if let Some(eq_pos) = after_value.find('=') { - let path_part = after_value[eq_pos + 1..].trim(); - // Extract quoted string - if let Some(start) = path_part.find('"') { - if let Some(end) = path_part[start + 1..].find('"') { - let path = &path_part[start + 1..start + 1 + end]; - return Some(PathBuf::from(path)); - } - } - } - } - } else { - // Simple format: "..." - if let Some(start) = value_part.find('"') { - if let Some(end) = value_part[start + 1..].find('"') { - let path = &value_part[start + 1..start + 1 + end]; - return Some(PathBuf::from(path)); - } - } - } - } - } + let env = table.get("env")?; + let entry = env.get("LLVM_SYS_140_PREFIX")?; + + // Simple string: LLVM_SYS_140_PREFIX = "/path" + if let Some(s) = entry.as_str() { + return Some(PathBuf::from(s)); + } + + // Inline table: LLVM_SYS_140_PREFIX = { value = "/path", force = true } + if let Some(t) = entry.as_table() + && let Some(v) = t.get("value").and_then(|v| v.as_str()) + { + return Some(PathBuf::from(v)); } None @@ -175,34 +155,37 @@ pub fn validate_llvm_config() -> ConfigValidation { /// it to `.cargo/config.toml` with `force=true`. /// /// Priority order: -/// 1. `~/.pecos/llvm` (PECOS-managed LLVM) -/// 2. `LLVM_SYS_140_PREFIX` environment variable -/// 3. System LLVM 14 (Homebrew, system paths, etc.) +/// 1. `~/.pecos/deps/llvm` (PECOS-managed LLVM, new path) +/// 2. `~/.pecos/llvm` (legacy path) +/// 3. `LLVM_SYS_140_PREFIX` environment variable +/// 4. System LLVM 14 (Homebrew, system paths, etc.) /// /// # Errors /// /// Returns an error if no suitable LLVM 14 installation could be found pub fn auto_configure_llvm(project_root: Option) -> Result { - // Priority 1: Check ~/.pecos/ for PECOS-managed LLVM + // Priority 1 & 2: Check ~/.pecos/deps/llvm and legacy ~/.pecos/llvm + let mut pecos_llvm_paths = Vec::new(); + if let Ok(deps_llvm) = crate::home::get_llvm_dir_path() { + pecos_llvm_paths.push(deps_llvm); + } + if let Ok(legacy_llvm) = crate::home::get_legacy_llvm_dir_path() { + pecos_llvm_paths.push(legacy_llvm); + } + #[cfg(target_os = "windows")] if let Some(home_dir) = dirs::home_dir() { - let pecos_dir = home_dir.join(".pecos"); - - #[cfg(target_os = "windows")] - let pecos_llvm_paths = vec![pecos_dir.join("LLVM-14"), pecos_dir.join("llvm")]; - - #[cfg(not(target_os = "windows"))] - let pecos_llvm_paths = vec![pecos_dir.join("llvm")]; + pecos_llvm_paths.push(home_dir.join(".pecos").join("LLVM-14")); + } - for pecos_llvm in pecos_llvm_paths { - if is_valid_llvm_14(&pecos_llvm) { - let project_root = project_root - .or_else(get_repo_root_from_manifest) - .or_else(find_cargo_project_root) - .ok_or_else(|| Error::Config("Could not find Cargo project root".into()))?; + for pecos_llvm in pecos_llvm_paths { + if is_valid_llvm_14(&pecos_llvm) { + let project_root = project_root + .or_else(get_repo_root_from_manifest) + .or_else(find_cargo_project_root) + .ok_or_else(|| Error::Config("Could not find Cargo project root".into()))?; - write_cargo_config(&project_root, &pecos_llvm, true)?; - return Ok(pecos_llvm); - } + write_cargo_config(&project_root, &pecos_llvm, true)?; + return Ok(pecos_llvm); } } diff --git a/crates/pecos-build/src/llvm/installer.rs b/crates/pecos-build/src/llvm/installer.rs index 9c246ebc0..a9bdb8638 100644 --- a/crates/pecos-build/src/llvm/installer.rs +++ b/crates/pecos-build/src/llvm/installer.rs @@ -32,7 +32,7 @@ const LLVM_CHECKSUMS: &[(&str, &str)] = &[ ), ]; -/// Install LLVM 14.0.6 to `~/.pecos/llvm/` +/// Install LLVM 14.0.6 to `~/.pecos/deps/llvm/` /// /// # Arguments /// * `force` - Force reinstall even if already present @@ -42,10 +42,7 @@ const LLVM_CHECKSUMS: &[(&str, &str)] = &[ /// /// Returns an error if installation fails pub fn install_llvm(force: bool, no_configure: bool) -> Result { - let llvm_dir = dirs::home_dir() - .ok_or_else(|| Error::HomeDir("Could not determine home directory".into()))? - .join(".pecos") - .join("llvm"); + let llvm_dir = crate::home::get_llvm_dir_path()?; // Check if already installed if !force && llvm_dir.exists() && is_valid_installation(&llvm_dir) { @@ -387,13 +384,10 @@ fn extract_7z(archive: &PathBuf, dest: &PathBuf) -> Result<()> { /// /// Returns an error if removal fails pub fn uninstall_llvm() -> Result<()> { - let llvm_dir = dirs::home_dir() - .ok_or_else(|| Error::HomeDir("Could not determine home directory".into()))? - .join(".pecos") - .join("llvm"); + let llvm_dir = crate::home::get_llvm_dir_path()?; if !llvm_dir.exists() { - println!("LLVM is not installed in ~/.pecos/llvm/"); + println!("LLVM is not installed in ~/.pecos/deps/llvm/"); return Ok(()); } diff --git a/crates/pecos-build/src/prompt.rs b/crates/pecos-build/src/prompt.rs new file mode 100644 index 000000000..ce650a333 --- /dev/null +++ b/crates/pecos-build/src/prompt.rs @@ -0,0 +1,70 @@ +//! Interactive prompt utilities for CLI commands +//! +//! Provides a simple Y/n confirmation prompt that respects TTY detection +//! and supports non-interactive overrides for CI environments. + +use std::io::{self, BufRead, IsTerminal, Write}; + +/// How to resolve prompts: interactively, or with a forced answer. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PromptMode { + /// Ask the user interactively (falls back to default if not a TTY). + Interactive, + /// Accept all prompts without asking. + AcceptAll, + /// Decline all prompts without asking. + DeclineAll, +} + +/// Prompt the user with a yes/no question. +/// +/// - `message`: The question to display (e.g. "Install LLVM 14 to ~/.pecos/deps/llvm/ (~400MB)?") +/// - `default_yes`: Whether the default answer is yes (`[Y/n]`) or no (`[y/N]`) +/// - `mode`: How to resolve the prompt +/// +/// In `Interactive` mode, returns the default if stdin is not a TTY (e.g. piped input, CI). +#[must_use] +pub fn confirm(message: &str, default_yes: bool, mode: PromptMode) -> bool { + match mode { + PromptMode::AcceptAll => return true, + PromptMode::DeclineAll => return false, + PromptMode::Interactive => {} + } + + // Non-interactive environment -> use default silently + if !io::stdin().is_terminal() { + return default_yes; + } + + let hint = if default_yes { "[Y/n]" } else { "[y/N]" }; + print!("{message} {hint} "); + io::stdout().flush().ok(); + + let mut input = String::new(); + if io::stdin().lock().read_line(&mut input).is_err() { + return default_yes; + } + + match input.trim().to_lowercase().as_str() { + "y" | "yes" => true, + "n" | "no" => false, + _ => default_yes, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn accept_all_ignores_default() { + assert!(confirm("test?", false, PromptMode::AcceptAll)); + assert!(confirm("test?", true, PromptMode::AcceptAll)); + } + + #[test] + fn decline_all_ignores_default() { + assert!(!confirm("test?", false, PromptMode::DeclineAll)); + assert!(!confirm("test?", true, PromptMode::DeclineAll)); + } +} diff --git a/crates/pecos-cuquantum-sys/build.rs b/crates/pecos-cuquantum-sys/build.rs index 8c4041f79..1a1612070 100644 --- a/crates/pecos-cuquantum-sys/build.rs +++ b/crates/pecos-cuquantum-sys/build.rs @@ -35,7 +35,7 @@ fn main() { log::info!("cuQuantum not found. Generating stub bindings."); log::info!("To use cuQuantum, either:"); log::info!(" 1. Set CUQUANTUM_ROOT environment variable"); - log::info!(" 2. Install cuQuantum to ~/.pecos/cuquantum/"); + log::info!(" 2. Install cuQuantum via: pecos install cuquantum"); log::info!(" 3. Install cuQuantum system-wide"); generate_stub_bindings(); diff --git a/crates/pecos-cuquantum/src/densitymat.rs b/crates/pecos-cuquantum/src/densitymat.rs index ea81ec6af..1dd3e78c8 100644 --- a/crates/pecos-cuquantum/src/densitymat.rs +++ b/crates/pecos-cuquantum/src/densitymat.rs @@ -70,7 +70,7 @@ impl CuDensityMat { return Err(CuQuantumError::NotAvailable( "cuQuantum SDK is not installed. To use GPU-accelerated simulators, install the cuQuantum SDK:\n\ 1. Set CUQUANTUM_ROOT environment variable, or\n\ - 2. Install to ~/.pecos/cuquantum/, or\n\ + 2. Install via: pecos install cuquantum, or\n\ 3. Install system-wide to /usr/local/cuquantum/" .into(), )); diff --git a/crates/pecos-cuquantum/src/stabilizer.rs b/crates/pecos-cuquantum/src/stabilizer.rs index cd173e695..ed20035d5 100644 --- a/crates/pecos-cuquantum/src/stabilizer.rs +++ b/crates/pecos-cuquantum/src/stabilizer.rs @@ -127,7 +127,7 @@ impl CuFrameSimulator { return Err(CuQuantumError::NotAvailable( "cuQuantum SDK is not installed. To use GPU-accelerated simulators, install the cuQuantum SDK:\n\ 1. Set CUQUANTUM_ROOT environment variable, or\n\ - 2. Install to ~/.pecos/cuquantum/, or\n\ + 2. Install via: pecos install cuquantum, or\n\ 3. Install system-wide to /usr/local/cuquantum/" .into(), )); @@ -507,7 +507,7 @@ impl CuStabilizer { return Err(CuQuantumError::NotAvailable( "cuQuantum SDK is not installed. To use GPU-accelerated simulators, install the cuQuantum SDK:\n\ 1. Set CUQUANTUM_ROOT environment variable, or\n\ - 2. Install to ~/.pecos/cuquantum/, or\n\ + 2. Install via: pecos install cuquantum, or\n\ 3. Install system-wide to /usr/local/cuquantum/" .into(), )); diff --git a/crates/pecos-cuquantum/src/statevec.rs b/crates/pecos-cuquantum/src/statevec.rs index 61e737085..04cc5858d 100644 --- a/crates/pecos-cuquantum/src/statevec.rs +++ b/crates/pecos-cuquantum/src/statevec.rs @@ -85,7 +85,7 @@ impl CuStateVec { return Err(CuQuantumError::NotAvailable( "cuQuantum SDK is not installed. To use GPU-accelerated simulators, install the cuQuantum SDK:\n\ 1. Set CUQUANTUM_ROOT environment variable, or\n\ - 2. Install to ~/.pecos/cuquantum/, or\n\ + 2. Install via: pecos install cuquantum, or\n\ 3. Install system-wide to /usr/local/cuquantum/" .into(), )); diff --git a/crates/pecos-cuquantum/src/tensornet.rs b/crates/pecos-cuquantum/src/tensornet.rs index 0fc5dee9d..19fb51350 100644 --- a/crates/pecos-cuquantum/src/tensornet.rs +++ b/crates/pecos-cuquantum/src/tensornet.rs @@ -46,7 +46,7 @@ impl CuTensorNet { return Err(crate::CuQuantumError::NotAvailable( "cuQuantum SDK is not installed. To use GPU-accelerated simulators, install the cuQuantum SDK:\n\ 1. Set CUQUANTUM_ROOT environment variable, or\n\ - 2. Install to ~/.pecos/cuquantum/, or\n\ + 2. Install via: pecos install cuquantum, or\n\ 3. Install system-wide to /usr/local/cuquantum/" .into(), )); diff --git a/crates/pecos-llvm/build.rs b/crates/pecos-llvm/build.rs index 107d73c3a..2c195a689 100644 --- a/crates/pecos-llvm/build.rs +++ b/crates/pecos-llvm/build.rs @@ -50,7 +50,7 @@ fn print_llvm_not_found_error_extended() { eprintln!(" cargo build"); eprintln!(); eprintln!(" The installer automatically configures PECOS."); - eprintln!(" (Downloads LLVM 14.0.6 to ~/.pecos/llvm/ - ~400MB, ~5 minutes)"); + eprintln!(" (Downloads LLVM 14.0.6 to ~/.pecos/deps/llvm/ - ~400MB, ~5 minutes)"); eprintln!(); #[cfg(target_os = "macos")] diff --git a/crates/pecos-qis/build.rs b/crates/pecos-qis/build.rs index 69d79c89f..4e3ec63b3 100644 --- a/crates/pecos-qis/build.rs +++ b/crates/pecos-qis/build.rs @@ -75,7 +75,7 @@ fn print_llvm_not_found_error_extended() { eprintln!(" cargo build"); eprintln!(); eprintln!(" The installer automatically configures PECOS."); - eprintln!(" (Downloads LLVM 14.0.6 to ~/.pecos/llvm/ - ~400MB, ~5 minutes)"); + eprintln!(" (Downloads LLVM 14.0.6 to ~/.pecos/deps/llvm/ - ~400MB, ~5 minutes)"); eprintln!(); #[cfg(target_os = "macos")] diff --git a/crates/pecos-qis/src/lib.rs b/crates/pecos-qis/src/lib.rs index 1d8a3e794..32ded0922 100644 --- a/crates/pecos-qis/src/lib.rs +++ b/crates/pecos-qis/src/lib.rs @@ -45,7 +45,7 @@ //! cargo build //! ``` //! -//! This takes ~5 minutes, downloads ~400MB, and installs to `~/.pecos/llvm`. +//! This takes ~5 minutes, downloads ~400MB, and installs to `~/.pecos/deps/llvm`. //! //! **Don't need QIR?** Disable LLVM: //! ```toml diff --git a/crates/pecos-quest/build_quest.rs b/crates/pecos-quest/build_quest.rs index 3e60a13d1..59e122936 100644 --- a/crates/pecos-quest/build_quest.rs +++ b/crates/pecos-quest/build_quest.rs @@ -18,18 +18,27 @@ use std::process::Command; /// Returns the CUDA installation path if found /// /// Search order: -/// 1. `~/.pecos/cuda/` (local installation via pecos install cuda) -/// 2. `CUDA_PATH` environment variable -/// 3. `nvcc` in PATH -/// 4. Standard system paths +/// 1. `~/.pecos/deps/cuda/` (new local installation via pecos install cuda) +/// 2. `~/.pecos/cuda/` (legacy path) +/// 3. `CUDA_PATH` environment variable +/// 4. `nvcc` in PATH +/// 5. Standard system paths fn detect_cuda_path() -> Option { - // 1. Check ~/.pecos/cuda/ first (local installation via pecos) + // 1-2. Check ~/.pecos/deps/cuda/ and legacy ~/.pecos/cuda/ if let Some(home) = dirs::home_dir() { - let pecos_cuda = home.join(".pecos").join("cuda"); - let nvcc_path = pecos_cuda.join("bin").join("nvcc"); - if nvcc_path.exists() { - info!("Found CUDA in ~/.pecos/cuda/ (installed via pecos)"); - return Some(pecos_cuda.to_string_lossy().to_string()); + let paths = [ + home.join(".pecos").join("deps").join("cuda"), + home.join(".pecos").join("cuda"), + ]; + for pecos_cuda in paths { + let nvcc_path = pecos_cuda.join("bin").join("nvcc"); + if nvcc_path.exists() { + info!( + "Found CUDA in {} (installed via pecos)", + pecos_cuda.display() + ); + return Some(pecos_cuda.to_string_lossy().to_string()); + } } } diff --git a/crates/pecos/src/bin/cli.rs b/crates/pecos/src/bin/cli.rs index b81206225..d6e1f30f7 100644 --- a/crates/pecos/src/bin/cli.rs +++ b/crates/pecos/src/bin/cli.rs @@ -6,6 +6,7 @@ #![allow(clippy::fn_params_excessive_bools)] +pub mod clean_cmd; pub mod cuda_cmd; pub mod cuquantum_cmd; pub mod docs_cmd; @@ -18,10 +19,12 @@ pub mod julia_cmd; pub mod list; pub mod llvm_cmd; pub mod manifest_cmd; +pub mod migrate_cmd; pub mod python_cmd; pub mod rust_cmd; pub mod selene_cmd; pub mod self_update_cmd; +pub mod setup_cmd; pub mod uninstall_cmd; pub mod upgrade_cmd; @@ -558,6 +561,43 @@ pub fn run_deps(command: DepsCommands) -> pecos_build::Result<()> { manifest_cmd::run(command) } +/// Run the clean command +/// +/// # Errors +/// +/// Returns an error if cleaning fails. +pub fn run_clean( + targets: &[String], + all: bool, + dry_run: bool, + yes: bool, +) -> pecos_build::Result<()> { + clean_cmd::run(targets, all, dry_run, yes) +} + +/// Run the setup command +/// +/// # Errors +/// +/// Returns an error if setup fails. +pub fn run_setup( + mode: pecos_build::prompt::PromptMode, + skip_llvm: bool, + skip_cuda: bool, + quiet: bool, +) -> pecos_build::Result<()> { + setup_cmd::run(mode, skip_llvm, skip_cuda, quiet) +} + +/// Run the migrate command +/// +/// # Errors +/// +/// Returns an error if migration fails. +pub fn run_migrate() -> pecos_build::Result<()> { + migrate_cmd::run() +} + /// Run the install command /// /// # Errors @@ -577,8 +617,8 @@ pub fn run_install( /// # Errors /// /// Returns an error if any target fails to uninstall. -pub fn run_uninstall(targets: &[String], all: bool) -> pecos_build::Result<()> { - uninstall_cmd::run(targets, all) +pub fn run_uninstall(targets: &[String], all: bool, yes: bool) -> pecos_build::Result<()> { + uninstall_cmd::run(targets, all, yes) } /// Run the upgrade command @@ -586,8 +626,13 @@ pub fn run_uninstall(targets: &[String], all: bool) -> pecos_build::Result<()> { /// # Errors /// /// Returns an error if any target fails to upgrade. -pub fn run_upgrade(targets: &[String], all: bool, no_configure: bool) -> pecos_build::Result<()> { - upgrade_cmd::run(targets, all, no_configure) +pub fn run_upgrade( + targets: &[String], + all: bool, + no_configure: bool, + yes: bool, +) -> pecos_build::Result<()> { + upgrade_cmd::run(targets, all, no_configure, yes) } /// Run the sys-info command diff --git a/crates/pecos/src/bin/cli/clean_cmd.rs b/crates/pecos/src/bin/cli/clean_cmd.rs new file mode 100644 index 000000000..a48ee31e8 --- /dev/null +++ b/crates/pecos/src/bin/cli/clean_cmd.rs @@ -0,0 +1,118 @@ +//! Implementation of the `clean` command +//! +//! Removes cached downloads and temporary files from `~/.pecos/`. + +use pecos_build::Result; +use pecos_build::errors::Error; +use pecos_build::prompt::{PromptMode, confirm}; +use std::fs; +use std::path::Path; + +/// Known clean targets +const KNOWN_TARGETS: &[&str] = &["cache", "tmp"]; + +/// Run the clean command. +pub fn run(targets: &[String], all: bool, dry_run: bool, yes: bool) -> Result<()> { + let targets: Vec<&str> = if all { + KNOWN_TARGETS.to_vec() + } else { + for target in targets { + let name = target.to_lowercase(); + if !KNOWN_TARGETS.contains(&name.as_str()) { + return Err(Error::Config(format!( + "Unknown clean target: '{target}'. Valid targets: {}", + KNOWN_TARGETS.join(", ") + ))); + } + } + targets.iter().map(String::as_str).collect() + }; + + let mut total_bytes = 0u64; + let mut dirs_to_clean: Vec<(&str, std::path::PathBuf, u64)> = Vec::new(); + + for &target in &targets { + let dir = match target { + "cache" => pecos_build::home::get_cache_dir_path()?, + "tmp" => pecos_build::home::get_tmp_dir_path()?, + _ => unreachable!(), + }; + if dir.exists() { + let size = dir_size(&dir).unwrap_or(0); + if size > 0 { + dirs_to_clean.push((target, dir, size)); + total_bytes += size; + } + } + } + + if dirs_to_clean.is_empty() { + println!("Nothing to clean."); + return Ok(()); + } + + println!("Will remove:"); + for (name, path, size) in &dirs_to_clean { + println!(" {name}: {} ({})", path.display(), format_bytes(*size)); + } + println!(" Total: {}", format_bytes(total_bytes)); + println!(); + + if dry_run { + println!("Dry run -- no files removed."); + return Ok(()); + } + + let mode = if yes { + PromptMode::AcceptAll + } else { + PromptMode::Interactive + }; + + if !confirm("Continue?", false, mode) { + println!("Cancelled."); + return Ok(()); + } + + for (name, path, _) in &dirs_to_clean { + print!("Cleaning {name}..."); + // Remove contents but keep the directory itself + for entry in fs::read_dir(path)?.flatten() { + if entry.path().is_dir() { + fs::remove_dir_all(entry.path())?; + } else { + fs::remove_file(entry.path())?; + } + } + println!(" done"); + } + + println!("Cleaned {}.", format_bytes(total_bytes)); + Ok(()) +} + +fn format_bytes(bytes: u64) -> String { + if bytes >= 1_073_741_824 { + format!("{:.1} GB", bytes as f64 / 1_073_741_824.0) + } else if bytes >= 1_048_576 { + format!("{:.0} MB", bytes as f64 / 1_048_576.0) + } else if bytes >= 1024 { + format!("{:.0} KB", bytes as f64 / 1024.0) + } else { + format!("{bytes} B") + } +} + +fn dir_size(path: &Path) -> Option { + let mut total = 0u64; + for entry in fs::read_dir(path).ok()? { + let entry = entry.ok()?; + let meta = entry.metadata().ok()?; + if meta.is_dir() { + total += dir_size(&entry.path())?; + } else { + total += meta.len(); + } + } + Some(total) +} diff --git a/crates/pecos/src/bin/cli/cuda_cmd.rs b/crates/pecos/src/bin/cli/cuda_cmd.rs index 7b1114177..0124be08f 100644 --- a/crates/pecos/src/bin/cli/cuda_cmd.rs +++ b/crates/pecos/src/bin/cli/cuda_cmd.rs @@ -22,7 +22,7 @@ fn run_check(quiet: bool) -> Result<()> { if let Some(cuda_path) = find_cuda() { if !quiet { // Determine if it's a local or system installation - let is_local = get_pecos_cuda_dir().is_some_and(|p| cuda_path.starts_with(&p)); + let is_local = get_pecos_cuda_dir().is_ok_and(|p| cuda_path.starts_with(&p)); let location = if is_local { "local" } else { "system" }; @@ -71,11 +71,11 @@ fn run_version() -> Result<()> { println!("Location: {}", cuda_path.display()); // Check if local or system - let is_local = get_pecos_cuda_dir().is_some_and(|p| cuda_path.starts_with(&p)); + let is_local = get_pecos_cuda_dir().is_ok_and(|p| cuda_path.starts_with(&p)); println!( "Type: {}", if is_local { - "local (~/.pecos/cuda/)" + "local (~/.pecos/deps/cuda/)" } else { "system" } diff --git a/crates/pecos/src/bin/cli/cuquantum_cmd.rs b/crates/pecos/src/bin/cli/cuquantum_cmd.rs index 1a459a8cd..e76113ad4 100644 --- a/crates/pecos/src/bin/cli/cuquantum_cmd.rs +++ b/crates/pecos/src/bin/cli/cuquantum_cmd.rs @@ -24,8 +24,7 @@ fn run_check(quiet: bool) -> Result<()> { if let Some(cuquantum_path) = find_cuquantum() { if !quiet { // Determine if it's a local or system installation - let is_local = - get_pecos_cuquantum_dir().is_some_and(|p| cuquantum_path.starts_with(&p)); + let is_local = get_pecos_cuquantum_dir().is_ok_and(|p| cuquantum_path.starts_with(&p)); let location = if is_local { "local" } else { "system" }; @@ -76,11 +75,11 @@ fn run_version() -> Result<()> { println!("Location: {}", cuquantum_path.display()); // Check if local or system - let is_local = get_pecos_cuquantum_dir().is_some_and(|p| cuquantum_path.starts_with(&p)); + let is_local = get_pecos_cuquantum_dir().is_ok_and(|p| cuquantum_path.starts_with(&p)); println!( "Type: {}", if is_local { - "local (~/.pecos/cuquantum/)" + "local (~/.pecos/deps/cuquantum/)" } else { "system" } diff --git a/crates/pecos/src/bin/cli/info.rs b/crates/pecos/src/bin/cli/info.rs index 2c39ac523..894092b32 100644 --- a/crates/pecos/src/bin/cli/info.rs +++ b/crates/pecos/src/bin/cli/info.rs @@ -3,6 +3,8 @@ #![allow(clippy::unnecessary_wraps)] use pecos_build::Result; +use pecos_build::cuda::{find_cuda, get_cuda_version}; +use pecos_build::cuquantum::{find_cuquantum, get_cuquantum_version}; use pecos_build::home::{get_cache_dir, get_deps_dir, get_llvm_dir, get_pecos_home}; use pecos_build::llvm::{find_llvm_14, get_llvm_version, get_repo_root_from_manifest}; use std::process::Command; @@ -46,6 +48,24 @@ pub fn run() -> Result<()> { } } + if let Ok(cuda_dir) = pecos_build::home::get_cuda_dir() { + print!(" CUDA: {}", cuda_dir.display()); + if cuda_dir.exists() { + println!(" (exists)"); + } else { + println!(" (not installed)"); + } + } + + if let Ok(cuq_dir) = pecos_build::home::get_cuquantum_dir() { + print!(" cuQ: {}", cuq_dir.display()); + if cuq_dir.exists() { + println!(" (exists)"); + } else { + println!(" (not installed)"); + } + } + if let Ok(deps_dir) = get_deps_dir() { print!(" Deps: {}", deps_dir.display()); if deps_dir.exists() { @@ -108,8 +128,20 @@ fn print_toolchain_status() { } // CUDA - let cuda_status = detect_cuda(); - println!(" CUDA: {cuda_status}"); + if let Some(cuda_path) = find_cuda() { + let version = get_cuda_version(&cuda_path).unwrap_or_else(|_| "unknown".to_string()); + println!(" CUDA: {} ({})", version, cuda_path.display()); + } else { + println!(" CUDA: not found"); + } + + // cuQuantum + if let Some(cuq_path) = find_cuquantum() { + let version = get_cuquantum_version(&cuq_path).unwrap_or_else(|_| "unknown".to_string()); + println!(" cuQuantum: {} ({})", version, cuq_path.display()); + } else { + println!(" cuQuantum: not found"); + } // Python let python_status = detect_python(); @@ -128,33 +160,6 @@ fn print_toolchain_status() { println!(" Go: {go_status}"); } -/// Detect CUDA availability -#[allow(clippy::collapsible_if)] -fn detect_cuda() -> String { - // Check for nvcc - if let Ok(output) = Command::new("nvcc").arg("--version").output() { - if output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - // Extract version from output like "Cuda compilation tools, release 12.0, V12.0.140" - if let Some(line) = stdout.lines().find(|l| l.contains("release")) { - if let Some(version) = line.split("release ").nth(1) { - if let Some(ver) = version.split(',').next() { - return format!("{ver} (nvcc found)"); - } - } - } - return "available (nvcc found)".to_string(); - } - } - - // Check for CUDA_PATH environment variable - if let Ok(cuda_path) = std::env::var("CUDA_PATH") { - return format!("CUDA_PATH={cuda_path}"); - } - - "not detected".to_string() -} - /// Detect Python installation #[allow(clippy::collapsible_if)] fn detect_python() -> String { diff --git a/crates/pecos/src/bin/cli/install_cmd.rs b/crates/pecos/src/bin/cli/install_cmd.rs index 62681f08e..80bb94510 100644 --- a/crates/pecos/src/bin/cli/install_cmd.rs +++ b/crates/pecos/src/bin/cli/install_cmd.rs @@ -36,7 +36,7 @@ pub fn run(targets: &[String], force: bool, all: bool, no_configure: bool) -> Re for (i, target) in targets.iter().enumerate() { if !force && is_available(target) { println!( - "[{}/{}] {target} is already available, skipping (use --force to reinstall)", + "[{}/{}] {target} is already available, skipping (use --force to install locally)", i + 1, total, ); @@ -52,7 +52,7 @@ pub fn run(targets: &[String], force: bool, all: bool, no_configure: bool) -> Re println!(); } - println!("All done."); + println!("All done. Run `just build` to build PECOS."); Ok(()) } diff --git a/crates/pecos/src/bin/cli/list.rs b/crates/pecos/src/bin/cli/list.rs index 43692ad53..eb8460652 100644 --- a/crates/pecos/src/bin/cli/list.rs +++ b/crates/pecos/src/bin/cli/list.rs @@ -3,10 +3,13 @@ #![allow(clippy::unnecessary_wraps)] use pecos_build::Result; +use pecos_build::cuda::find_cuda; +use pecos_build::cuquantum::find_cuquantum; use pecos_build::deps::list_dependencies; -use pecos_build::home::{get_cache_dir, get_deps_dir, get_llvm_dir}; +use pecos_build::home::{get_cache_dir, get_deps_dir}; use pecos_build::llvm::{find_llvm_14, get_llvm_version, get_repo_root_from_manifest}; use std::fs; +use std::path::Path; /// Run the list command pub fn run(verbose: bool) -> Result<()> { @@ -15,81 +18,123 @@ pub fn run(verbose: bool) -> Result<()> { println!(); // LLVM status - println!("LLVM 14:"); let repo_root = get_repo_root_from_manifest(); if let Some(llvm_path) = find_llvm_14(repo_root) { - print!(" Status: Installed at {}", llvm_path.display()); - if let Ok(version) = get_llvm_version(&llvm_path) { - println!(" (version {version})"); - } else { - println!(); - } + let version = get_llvm_version(&llvm_path) + .map(|v| format!(" ({v})")) + .unwrap_or_default(); + println!("LLVM 14: {}{version}", llvm_path.display()); } else { - println!(" Status: Not found"); - println!(" Install with: pecos install llvm"); + println!("LLVM 14: not found (install with: pecos install llvm)"); + } + + // CUDA status + if let Some(cuda_path) = find_cuda() { + println!("CUDA: {}", cuda_path.display()); + } else { + println!("CUDA: not found"); } - println!(); - // List available dependencies - println!("Available Dependencies:"); - for dep in list_dependencies() { - println!(" {}: {} - {}", dep.name, dep.version, dep.description); + // cuQuantum status + if let Some(cuq_path) = find_cuquantum() { + println!("cuQuantum: {}", cuq_path.display()); + } else { + println!("cuQuantum: not found"); } println!(); - // List extracted sources and cached archives if verbose { - println!("Extracted Sources (~/.pecos/deps/):"); + // Extracted deps with sizes + println!("Installed (~/.pecos/deps/):"); if let Ok(deps_dir) = get_deps_dir() { if deps_dir.exists() { - let mut found = false; - if let Ok(entries) = fs::read_dir(&deps_dir) { - for entry in entries.flatten() { - if entry.path().is_dir() { - println!(" {}", entry.file_name().to_string_lossy()); - found = true; - } - } - } - if !found { + let mut entries: Vec<_> = fs::read_dir(&deps_dir) + .ok() + .into_iter() + .flatten() + .flatten() + .filter(|e| e.path().is_dir()) + .collect(); + entries.sort_by_key(std::fs::DirEntry::file_name); + + if entries.is_empty() { println!(" (none)"); + } else { + for entry in &entries { + let size = dir_size_display(&entry.path()); + println!(" {:30} {size}", entry.file_name().to_string_lossy()); + } } } else { - println!(" (deps directory not created yet)"); + println!(" (not created yet)"); } } println!(); - println!("Downloaded Archives (~/.pecos/cache/):"); + // Cached downloads with sizes + println!("Cached downloads (~/.pecos/cache/):"); if let Ok(cache_dir) = get_cache_dir() { if cache_dir.exists() { - let mut found = false; - if let Ok(entries) = fs::read_dir(&cache_dir) { - for entry in entries.flatten() { - if entry.path().is_file() { - println!(" {}", entry.file_name().to_string_lossy()); - found = true; - } - } - } - if !found { + let mut entries: Vec<_> = fs::read_dir(&cache_dir) + .ok() + .into_iter() + .flatten() + .flatten() + .filter(|e| e.path().is_file()) + .collect(); + entries.sort_by_key(std::fs::DirEntry::file_name); + + if entries.is_empty() { println!(" (none)"); + } else { + for entry in &entries { + let size = entry + .metadata() + .map_or_else(|_| "?".to_string(), |m| format_bytes(m.len())); + println!(" {:50} {size}", entry.file_name().to_string_lossy()); + } } } else { - println!(" (cache directory not created yet)"); + println!(" (not created yet)"); } } - println!(); - - println!("LLVM Directory:"); - if let Ok(llvm_dir) = get_llvm_dir() { - if llvm_dir.exists() { - println!(" {}", llvm_dir.display()); - } else { - println!(" (not installed)"); - } + } else { + // Non-verbose: just list available deps from manifest + println!("Available Dependencies:"); + for dep in list_dependencies() { + println!(" {}: {} - {}", dep.name, dep.version, dep.description); } } Ok(()) } + +fn dir_size_display(path: &Path) -> String { + dir_size(path).map_or_else(|| "?".to_string(), format_bytes) +} + +fn format_bytes(bytes: u64) -> String { + if bytes >= 1_073_741_824 { + format!("{:.1} GB", bytes as f64 / 1_073_741_824.0) + } else if bytes >= 1_048_576 { + format!("{:.0} MB", bytes as f64 / 1_048_576.0) + } else if bytes >= 1024 { + format!("{:.0} KB", bytes as f64 / 1024.0) + } else { + format!("{bytes} B") + } +} + +fn dir_size(path: &Path) -> Option { + let mut total = 0u64; + for entry in fs::read_dir(path).ok()? { + let entry = entry.ok()?; + let meta = entry.metadata().ok()?; + if meta.is_dir() { + total += dir_size(&entry.path())?; + } else { + total += meta.len(); + } + } + Some(total) +} diff --git a/crates/pecos/src/bin/cli/migrate_cmd.rs b/crates/pecos/src/bin/cli/migrate_cmd.rs new file mode 100644 index 000000000..7762f09f5 --- /dev/null +++ b/crates/pecos/src/bin/cli/migrate_cmd.rs @@ -0,0 +1,36 @@ +//! Implementation of the `migrate` command +//! +//! Moves legacy top-level installs (LLVM, CUDA, cuQuantum) from `~/.pecos/` +//! into `~/.pecos/deps/` to match the new directory layout. + +use pecos_build::Result; +use pecos_build::home::{find_legacy_deps, migrate_legacy_dep}; + +/// Run the migrate command. +pub fn run() -> Result<()> { + let legacy = find_legacy_deps()?; + + if legacy.is_empty() { + println!("Nothing to migrate. All dependencies are already under ~/.pecos/deps/."); + return Ok(()); + } + + println!("Migrating legacy dependencies to ~/.pecos/deps/:"); + for dep in &legacy { + print!( + " {} : {} -> {}", + dep.name, + dep.old.display(), + dep.new.display() + ); + migrate_legacy_dep(dep)?; + println!(" ... done"); + } + + println!(); + println!( + "Migration complete. You may want to run `pecos llvm configure` to update .cargo/config.toml." + ); + + Ok(()) +} diff --git a/crates/pecos/src/bin/cli/setup_cmd.rs b/crates/pecos/src/bin/cli/setup_cmd.rs new file mode 100644 index 000000000..428da4ac0 --- /dev/null +++ b/crates/pecos/src/bin/cli/setup_cmd.rs @@ -0,0 +1,182 @@ +//! Implementation of the `setup` command +//! +//! Detects missing optional dependencies and interactively prompts the user +//! to install them. Designed to be called before `build` so that the build +//! environment is ready. + +use pecos_build::Result; +use pecos_build::prompt::{PromptMode, confirm}; + +/// Run the setup command. +/// +/// Checks for LLVM, CUDA, and cuQuantum and offers to install each one +/// that is missing. Prompt defaults follow the principle of least surprise: +/// +/// - LLVM: default **yes** (required for full build, ~400 MB) +/// - CUDA: default **no** (large download ~4 GB, needs NVIDIA GPU) +/// - cuQuantum: default **yes** when CUDA is present (small, almost always wanted) +/// +/// When `quiet` is true, suppresses output when all deps are already found. +/// Prompts and install output are still shown when something needs action. +pub fn run(mode: PromptMode, skip_llvm: bool, skip_cuda: bool, quiet: bool) -> Result<()> { + // Check for legacy installs that should be migrated + check_legacy_deps(mode)?; + + if !skip_llvm { + setup_llvm(mode, quiet)?; + } + + if !skip_cuda { + setup_cuda(mode, quiet)?; + } + + // cuQuantum: only relevant when CUDA is available + if !skip_cuda && pecos_build::cuda::find_cuda().is_some() { + setup_cuquantum(mode, quiet)?; + } + + if !quiet { + println!("Setup complete."); + } + Ok(()) +} + +// ── Migration ────────────────────────────────────────────────────────────── + +fn check_legacy_deps(mode: PromptMode) -> Result<()> { + let legacy = pecos_build::home::find_legacy_deps()?; + if legacy.is_empty() { + return Ok(()); + } + + // Always print migration prompts regardless of quiet flag + println!("Found dependencies at legacy paths:"); + for dep in &legacy { + println!(" {} -> {}", dep.old.display(), dep.new.display()); + } + + if confirm("Migrate to ~/.pecos/deps/?", true, mode) { + for dep in &legacy { + print!(" Moving {}...", dep.name); + pecos_build::home::migrate_legacy_dep(dep)?; + println!(" done"); + } + println!(); + } else { + println!("Skipping migration. Run `pecos migrate` later to move them."); + println!(); + } + + Ok(()) +} + +// ── LLVM ──────────────────────────────────────────────────────────────────── + +fn setup_llvm(mode: PromptMode, quiet: bool) -> Result<()> { + if pecos_build::llvm::find_llvm_14(None).is_some() { + if !quiet { + println!("LLVM 14: found"); + } + ensure_llvm_configured(quiet); + return Ok(()); + } + + if confirm( + "LLVM 14 not found. Install to ~/.pecos/deps/llvm/ (~400 MB)?", + true, + mode, + ) { + pecos_build::llvm::installer::install_llvm(false, false)?; + } else { + println!("Skipping LLVM. QIR features will not be available."); + } + + Ok(()) +} + +// ── CUDA ──────────────────────────────────────────────────────────────────── + +fn setup_cuda(mode: PromptMode, quiet: bool) -> Result<()> { + if pecos_build::cuda::find_cuda().is_some() { + if !quiet { + println!("CUDA: found"); + } + return Ok(()); + } + + // Only offer CUDA on platforms where it is supported + if !cuda_platform_supported() { + if !quiet { + println!("CUDA: skipped (not supported on this platform)"); + } + return Ok(()); + } + + if confirm( + "CUDA not found. Install to ~/.pecos/deps/cuda/ (~4 GB)?", + false, + mode, + ) { + pecos_build::cuda::installer::install_cuda(false)?; + } else { + println!("Skipping CUDA. GPU features will not be available."); + } + + Ok(()) +} + +// ── cuQuantum ─────────────────────────────────────────────────────────────── + +fn setup_cuquantum(mode: PromptMode, quiet: bool) -> Result<()> { + if pecos_build::cuquantum::find_cuquantum().is_some() { + if !quiet { + println!("cuQuantum: found"); + } + return Ok(()); + } + + if confirm( + "cuQuantum not found. Install to ~/.pecos/deps/cuquantum/?", + true, + mode, + ) { + pecos_build::cuquantum::installer::install_cuquantum(false)?; + } else { + println!("Skipping cuQuantum."); + } + + Ok(()) +} + +// ── Helpers ───────────────────────────────────────────────────────────────── + +/// Ensure LLVM is configured in `.cargo/config.toml` after detection/install. +fn ensure_llvm_configured(quiet: bool) { + let config = pecos_build::llvm::config::validate_llvm_config(); + if config.is_healthy() { + return; + } + if !quiet { + println!("LLVM found but not configured, configuring..."); + } + match pecos_build::llvm::config::auto_configure_llvm(None) { + Ok(path) => { + if !quiet { + println!( + "Updated .cargo/config.toml with LLVM path: {}", + path.display() + ); + } + } + Err(e) => { + // Always show errors + eprintln!("Warning: could not auto-configure LLVM: {e}"); + config.print_warnings(); + } + } +} + +/// Returns true if the current platform supports CUDA installation. +fn cuda_platform_supported() -> bool { + cfg!(target_os = "linux") || cfg!(target_os = "windows") +} diff --git a/crates/pecos/src/bin/cli/uninstall_cmd.rs b/crates/pecos/src/bin/cli/uninstall_cmd.rs index b831a8364..e1946b912 100644 --- a/crates/pecos/src/bin/cli/uninstall_cmd.rs +++ b/crates/pecos/src/bin/cli/uninstall_cmd.rs @@ -2,12 +2,14 @@ use pecos_build::Result; use pecos_build::errors::Error; +use pecos_build::prompt::{PromptMode, confirm}; +use std::path::PathBuf; /// Known uninstallable targets const KNOWN_TARGETS: &[&str] = &["cuda", "llvm", "cuquantum"]; /// Run the uninstall command -pub fn run(targets: &[String], all: bool) -> Result<()> { +pub fn run(targets: &[String], all: bool, yes: bool) -> Result<()> { let targets: Vec<&str> = if all { KNOWN_TARGETS.to_vec() } else { @@ -32,8 +34,40 @@ pub fn run(targets: &[String], all: bool) -> Result<()> { ordered }; - let total = targets.len(); - for (i, target) in targets.iter().enumerate() { + // Collect what will actually be removed (skip targets that aren't installed) + let mut removals: Vec<(&str, PathBuf)> = Vec::new(); + for &target in &targets { + if let Some(path) = installed_path(target) { + removals.push((target, path)); + } + } + + if removals.is_empty() { + println!("Nothing to uninstall."); + return Ok(()); + } + + // Show what will be removed and ask for confirmation + println!("This will remove:"); + for (name, path) in &removals { + let size = dir_size_display(path); + println!(" {name}: {} ({size})", path.display()); + } + println!(); + + let mode = if yes { + PromptMode::AcceptAll + } else { + PromptMode::Interactive + }; + + if !confirm("Continue?", false, mode) { + println!("Cancelled."); + return Ok(()); + } + + let total = removals.len(); + for (i, (target, _)) in removals.iter().enumerate() { println!("[{}/{}] Uninstalling {target}...", i + 1, total); uninstall_target(target)?; println!(); @@ -43,6 +77,44 @@ pub fn run(targets: &[String], all: bool) -> Result<()> { Ok(()) } +/// Get the installed path for a target, or None if not locally installed. +fn installed_path(target: &str) -> Option { + let path = match target { + "cuda" => pecos_build::home::get_cuda_dir_path().ok()?, + "llvm" => pecos_build::home::get_llvm_dir_path().ok()?, + "cuquantum" => pecos_build::home::get_cuquantum_dir_path().ok()?, + _ => return None, + }; + if path.exists() { Some(path) } else { None } +} + +/// Get a human-readable size string for a directory. +fn dir_size_display(path: &PathBuf) -> String { + match dir_size(path) { + Some(bytes) if bytes >= 1_073_741_824 => { + format!("{:.1} GB", bytes as f64 / 1_073_741_824.0) + } + Some(bytes) if bytes >= 1_048_576 => format!("{:.0} MB", bytes as f64 / 1_048_576.0), + Some(bytes) => format!("{bytes} bytes"), + None => "unknown size".to_string(), + } +} + +/// Recursively compute directory size in bytes. +fn dir_size(path: &PathBuf) -> Option { + let mut total = 0u64; + for entry in std::fs::read_dir(path).ok()? { + let entry = entry.ok()?; + let meta = entry.metadata().ok()?; + if meta.is_dir() { + total += dir_size(&entry.path())?; + } else { + total += meta.len(); + } + } + Some(total) +} + /// Uninstall a single target fn uninstall_target(target: &str) -> Result<()> { match target { diff --git a/crates/pecos/src/bin/cli/upgrade_cmd.rs b/crates/pecos/src/bin/cli/upgrade_cmd.rs index a98ebaa45..9be74bdf8 100644 --- a/crates/pecos/src/bin/cli/upgrade_cmd.rs +++ b/crates/pecos/src/bin/cli/upgrade_cmd.rs @@ -4,12 +4,13 @@ use pecos_build::Result; use pecos_build::errors::Error; +use pecos_build::prompt::{PromptMode, confirm}; /// Known upgradeable targets const KNOWN_TARGETS: &[&str] = &["cuda", "llvm", "cuquantum"]; /// Run the upgrade command -pub fn run(targets: &[String], all: bool, no_configure: bool) -> Result<()> { +pub fn run(targets: &[String], all: bool, no_configure: bool, yes: bool) -> Result<()> { let targets: Vec<&str> = if all { KNOWN_TARGETS.to_vec() } else { @@ -34,6 +35,23 @@ pub fn run(targets: &[String], all: bool, no_configure: bool) -> Result<()> { ordered }; + println!("This will force-reinstall:"); + for target in &targets { + println!(" {target}"); + } + println!(); + + let mode = if yes { + PromptMode::AcceptAll + } else { + PromptMode::Interactive + }; + + if !confirm("Continue?", false, mode) { + println!("Cancelled."); + return Ok(()); + } + let total = targets.len(); for (i, target) in targets.iter().enumerate() { println!("[{}/{}] Upgrading {target}...", i + 1, total); @@ -42,7 +60,7 @@ pub fn run(targets: &[String], all: bool, no_configure: bool) -> Result<()> { println!(); } - println!("All done."); + println!("All done. Run `just build` to rebuild PECOS."); Ok(()) } diff --git a/crates/pecos/src/bin/pecos.rs b/crates/pecos/src/bin/pecos.rs index fc604db79..bc5ff8f5b 100644 --- a/crates/pecos/src/bin/pecos.rs +++ b/crates/pecos/src/bin/pecos.rs @@ -129,6 +129,61 @@ enum Commands { #[command(subcommand)] command: DepsCommands, }, + /// Set up build environment (detect and install missing dependencies) + /// + /// Interactively checks for LLVM, CUDA, and cuQuantum and offers to + /// install each one that is missing. Use --yes to accept all prompts + /// (for CI) or --no to decline all (for a lite build). + /// + /// Example: pecos setup + /// Example: pecos setup --yes + Setup { + /// Accept all prompts without asking (for CI) + #[arg(long, conflicts_with = "no")] + yes: bool, + + /// Decline all prompts without asking (lite mode) + #[arg(long, conflicts_with = "yes")] + no: bool, + + /// Skip LLVM setup + #[arg(long)] + skip_llvm: bool, + + /// Skip CUDA setup + #[arg(long)] + skip_cuda: bool, + + /// Suppress output when all dependencies are already found + #[arg(short, long)] + quiet: bool, + }, + /// Migrate legacy deps from ~/.pecos/ to ~/.pecos/deps/ + /// + /// Moves LLVM, CUDA, and cuQuantum installations from the old top-level + /// paths into the unified deps/ directory. + Migrate, + /// Clean cached downloads and temporary files from ~/.pecos/ + /// + /// Example: pecos clean cache + /// Example: pecos clean --all --dry-run + Clean { + /// What to clean: cache, tmp + #[arg(required_unless_present = "all")] + targets: Vec, + + /// Clean all targets + #[arg(long)] + all: bool, + + /// Show what would be removed without removing + #[arg(long)] + dry_run: bool, + + /// Skip confirmation prompt + #[arg(long, short)] + yes: bool, + }, /// Install optional dependencies (cuda, llvm, cuquantum) /// /// Example: pecos install cuda cuquantum @@ -160,6 +215,10 @@ enum Commands { /// Uninstall all optional dependencies #[arg(long)] all: bool, + + /// Skip confirmation prompt + #[arg(long, short)] + yes: bool, }, /// Upgrade optional dependencies (force reinstall) /// @@ -176,6 +235,10 @@ enum Commands { /// Skip automatic configuration after installation (applies to llvm) #[arg(long)] no_configure: bool, + + /// Skip confirmation prompt + #[arg(long, short)] + yes: bool, }, /// Show system tools and project info #[command(name = "sys-info")] @@ -671,18 +734,42 @@ fn main() -> Result<(), Box> { Commands::Selene { command } => cli::run_selene(command.clone())?, Commands::Features { command } => cli::run_features(command.clone())?, Commands::Deps { command } => cli::run_deps(command.clone())?, + Commands::Setup { + yes, + no, + skip_llvm, + skip_cuda, + quiet, + } => { + let mode = if *yes { + pecos_build::prompt::PromptMode::AcceptAll + } else if *no { + pecos_build::prompt::PromptMode::DeclineAll + } else { + pecos_build::prompt::PromptMode::Interactive + }; + cli::run_setup(mode, *skip_llvm, *skip_cuda, *quiet)?; + } + Commands::Migrate => cli::run_migrate()?, + Commands::Clean { + targets, + all, + dry_run, + yes, + } => cli::run_clean(targets, *all, *dry_run, *yes)?, Commands::Install { targets, force, all, no_configure, } => cli::run_install(targets, *force, *all, *no_configure)?, - Commands::Uninstall { targets, all } => cli::run_uninstall(targets, *all)?, + Commands::Uninstall { targets, all, yes } => cli::run_uninstall(targets, *all, *yes)?, Commands::Upgrade { targets, all, no_configure, - } => cli::run_upgrade(targets, *all, *no_configure)?, + yes, + } => cli::run_upgrade(targets, *all, *no_configure, *yes)?, Commands::SysInfo => cli::run_sys_info()?, Commands::List { verbose } => cli::run_list(*verbose)?, Commands::Self_ { command } => cli::run_self(command.clone())?, diff --git a/docs/development/dev-tools.md b/docs/development/dev-tools.md index 2b7ef3bff..f8f06953f 100644 --- a/docs/development/dev-tools.md +++ b/docs/development/dev-tools.md @@ -21,9 +21,9 @@ pecos python build # Build pecos-rslib with maturin pecos python test # Run pytest # Dependency installation (apt-like) -pecos install llvm # Install LLVM 14 to ~/.pecos/llvm/ -pecos install cuda # Install CUDA Toolkit to ~/.pecos/cuda/ -pecos install cuquantum # Install cuQuantum SDK to ~/.pecos/cuquantum/ +pecos install llvm # Install LLVM 14 to ~/.pecos/deps/llvm/ +pecos install cuda # Install CUDA Toolkit to ~/.pecos/deps/cuda/ +pecos install cuquantum # Install cuQuantum SDK to ~/.pecos/deps/cuquantum/ pecos install --all # Install all optional dependencies pecos uninstall llvm # Uninstall LLVM pecos upgrade llvm # Upgrade (force reinstall) LLVM @@ -138,7 +138,7 @@ pecos install llvm --force pecos install llvm --no-configure ``` -This downloads and installs LLVM 14 to `~/.pecos/llvm/`. +This downloads and installs LLVM 14 to `~/.pecos/deps/llvm/`. ### Check LLVM Status diff --git a/docs/user-guide/cli.md b/docs/user-guide/cli.md index 2f3753f54..6f87a8a9c 100644 --- a/docs/user-guide/cli.md +++ b/docs/user-guide/cli.md @@ -133,7 +133,7 @@ Checking PECOS installation... [OK] PHIR/JSON support: available [OK] Selene runtime: available [!!] LLVM/QIS support: not compiled (optional) - [OK] LLVM 14: 14.0.6 at /home/user/.pecos/llvm + [OK] LLVM 14: 14.0.6 at /home/user/.pecos/deps/llvm [OK] Test circuit: execution successful Suggestions: diff --git a/docs/user-guide/llvm-setup.md b/docs/user-guide/llvm-setup.md index b6cde963a..677d3c8dc 100644 --- a/docs/user-guide/llvm-setup.md +++ b/docs/user-guide/llvm-setup.md @@ -26,7 +26,7 @@ If you don't need QIS LLVM IR/QIR execution features, you can skip LLVM installa Use the `pecos-llvm` CLI tool to automatically download and install LLVM 14.0.6: ```bash -# Install LLVM 14.0.6 to ~/.pecos/llvm/ (~400MB, ~5 minutes) +# Install LLVM 14.0.6 to ~/.pecos/deps/llvm/ (~400MB, ~5 minutes) cargo run -p pecos --features cli -- install llvm # Build PECOS with LLVM support @@ -36,7 +36,7 @@ cargo build --features llvm The `install` command automatically: - Downloads the correct LLVM binary for your platform -- Extracts it to `~/.pecos/llvm/` +- Extracts it to `~/.pecos/deps/llvm/` - Configures PECOS by updating `.cargo/config.toml` This is the **recommended approach** for all platforms, especially Windows where system package managers may not provide LLVM 14 development files. @@ -116,7 +116,7 @@ The `pecos llvm` CLI tool provides several useful commands: ### `install` -Download and install LLVM 14.0.6 to `~/.pecos/llvm/`: +Download and install LLVM 14.0.6 to `~/.pecos/deps/llvm/`: ```bash cargo run -p pecos --features cli -- install llvm @@ -217,8 +217,8 @@ LLVM_SYS_140_PREFIX = { value = "/path/to/llvm", force = true } The `pecos-llvm` tool searches for LLVM 14 in this order: 1. **Home directory:** - - Windows: `~/.pecos/LLVM-14` or `~/.pecos/llvm` - - Unix: `~/.pecos/llvm` + - Windows: `~/.pecos/deps/llvm` + - Unix: `~/.pecos/deps/llvm` 2. **Project-local:** `/llvm/` @@ -298,13 +298,15 @@ LLVM_SYS_140_PREFIX = { value = "/path/to/llvm", force = true } ## PECOS Home Directory -LLVM is installed to `~/.pecos/llvm/`, which is part of the PECOS home directory structure: +LLVM is installed to `~/.pecos/deps/llvm/`, which is part of the PECOS home directory structure: ``` ~/.pecos/ -├── llvm/ # LLVM-14 installation -├── deps/ # Other C++ dependencies (decoders, simulators) -└── cache/ # Build artifacts +├── deps/ +│ ├── llvm/ # LLVM-14 installation +│ ├── cuda/ # CUDA Toolkit +│ └── cuquantum/ # cuQuantum SDK +└── cache/ # Build artifacts ``` You can override the PECOS home location using the `PECOS_HOME` environment variable or in `.cargo/config.toml`: