From f2ce01d9e11b4dd784919499cb9042e7fdb3c6db Mon Sep 17 00:00:00 2001 From: Levi Cook Date: Thu, 26 Jun 2025 21:28:51 -0600 Subject: [PATCH 1/2] feat: Add ecosystem packages for instant Solana program access Introduces pre-built ELF exports for popular Solana programs, eliminating build time and dependencies for common use cases. Core Features: - Ecosystem packages with git submodule-based versioning - Enhanced configuration supporting custom constants/targets - Automated release workflow (local prep + GitHub CI) - Pre-built SPL Token program (standard + Pinocchio optimized) Architecture Improvements: - Refactored SolanaProgram with configurable constant names - Enhanced workspace discovery with path resolution - Moved program deduplication to dedicated module - Added comprehensive test coverage for new features Breaking Changes: - SolanaProgram.constant_name() removed (now a field) - load_workspaces() signature changed (added config_file_dir param) Documentation: - User-focused ecosystem package docs - Updated README with ecosystem table - Comprehensive release automation guide --- .github/workflows/ecosystem-release.yml | 193 +++++++++++++++++++ .gitmodules | 3 + Cargo.lock | 7 + Cargo.toml | 7 +- Makefile | 26 +++ README.md | 19 +- docs/ecosystem.md | 241 ++++++++++++++++++++++++ docs/modes/magic.md | 4 +- ecosystem/solana-spl-token/Cargo.toml | 35 ++++ ecosystem/solana-spl-token/build.rs | 3 + ecosystem/solana-spl-token/src/lib.rs | 22 +++ ecosystem/solana-spl-token/upstream | 1 + scripts/ecosystem-prepare.sh | 94 +++++++++ scripts/prepare-ecosystem-release | 94 +++++++++ scripts/{release.sh => release} | 0 src/builder.rs | 21 ++- src/codegen.rs | 78 ++++++-- src/config.rs | 195 +++++++++++++++++-- src/lib.rs | 156 ++------------- src/programs.rs | 129 ++++++++++--- src/workspace.rs | 80 +++++++- tests/laser_eyes_mode_integration.rs | 8 +- tests/permissive_mode_integration.rs | 17 ++ 23 files changed, 1214 insertions(+), 219 deletions(-) create mode 100644 .github/workflows/ecosystem-release.yml create mode 100644 .gitmodules create mode 100644 docs/ecosystem.md create mode 100644 ecosystem/solana-spl-token/Cargo.toml create mode 100644 ecosystem/solana-spl-token/build.rs create mode 100644 ecosystem/solana-spl-token/src/lib.rs create mode 160000 ecosystem/solana-spl-token/upstream create mode 100644 scripts/ecosystem-prepare.sh create mode 100755 scripts/prepare-ecosystem-release rename scripts/{release.sh => release} (100%) diff --git a/.github/workflows/ecosystem-release.yml b/.github/workflows/ecosystem-release.yml new file mode 100644 index 0000000..e23cddb --- /dev/null +++ b/.github/workflows/ecosystem-release.yml @@ -0,0 +1,193 @@ +name: Ecosystem Release + +on: + push: + tags: + - "ecosystem/*/v*.*.*" # ecosystem/solana-spl-token/v3.4.0 + +env: + CARGO_TERM_COLOR: always + +jobs: + # Extract ecosystem info from tag + setup: + name: Setup Release Info + runs-on: ubuntu-latest + outputs: + ecosystem_name: ${{ steps.parse.outputs.ecosystem_name }} + package_name: ${{ steps.parse.outputs.package_name }} + version: ${{ steps.parse.outputs.version }} + manifest_path: ${{ steps.parse.outputs.manifest_path }} + steps: + - name: Parse ecosystem tag + id: parse + run: | + # Tag format: ecosystem/solana-spl-token/v3.4.0 + TAG="${GITHUB_REF#refs/tags/}" + echo "Full tag: $TAG" + + # Extract ecosystem name (solana-spl-token) + ECOSYSTEM_NAME=$(echo "$TAG" | cut -d'/' -f2) + echo "ecosystem_name=$ECOSYSTEM_NAME" >> $GITHUB_OUTPUT + + # Extract version (3.4.0) + VERSION=$(echo "$TAG" | cut -d'/' -f3 | sed 's/^v//') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + # Generate package name (elf-magic-solana-spl-token) + PACKAGE_NAME="elf-magic-$ECOSYSTEM_NAME" + echo "package_name=$PACKAGE_NAME" >> $GITHUB_OUTPUT + + # Generate manifest path + MANIFEST_PATH="ecosystem/$ECOSYSTEM_NAME/Cargo.toml" + echo "manifest_path=$MANIFEST_PATH" >> $GITHUB_OUTPUT + + echo "Ecosystem: $ECOSYSTEM_NAME" + echo "Package: $PACKAGE_NAME" + echo "Version: $VERSION" + echo "Manifest: $MANIFEST_PATH" + + # Validate ecosystem package in Docker environment + validate: + name: Ecosystem Validation + runs-on: ubuntu-latest + needs: setup + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive # Important: fetch git submodules + + # Cache Cargo registry + - name: Cache Cargo registry + uses: actions/cache@v4 + with: + path: ~/.cargo + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-registry- + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + # Install Solana CLI for cargo build-sbf + - name: Install Solana CLI + run: | + sh -c "$(curl -sSfL https://release.solana.com/stable/install)" + echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH + solana --version + cargo build-sbf --version + + - name: Validate ecosystem package + run: | + echo "๐Ÿ”จ Building ecosystem package: ${{ needs.setup.outputs.package_name }} v${{ needs.setup.outputs.version }}" + cargo build --manifest-path "${{ needs.setup.outputs.manifest_path }}" + + echo "๐Ÿงช Running tests..." + cargo test --manifest-path "${{ needs.setup.outputs.manifest_path }}" + + echo "๐Ÿ“Ž Running clippy..." + cargo clippy --manifest-path "${{ needs.setup.outputs.manifest_path }}" --all-targets -- -D warnings + + echo "๐Ÿ” Validating generated ELF constants..." + GENERATED_LIB="ecosystem/${{ needs.setup.outputs.ecosystem_name }}/src/lib.rs" + if [ ! -f "$GENERATED_LIB" ]; then + echo "โŒ Generated lib.rs not found" + exit 1 + fi + + if ! grep -q "pub const.*_ELF: &\[u8\]" "$GENERATED_LIB"; then + echo "โŒ No ELF constants found in generated code" + exit 1 + fi + + echo "โœ… ELF constants validation passed" + + echo "๐Ÿ“ฆ Publish dry run..." + cargo publish --manifest-path "${{ needs.setup.outputs.manifest_path }}" --dry-run + + # Publish to crates.io + publish: + name: Publish to crates.io + runs-on: ubuntu-latest + needs: [setup, validate] + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + # Install Solana CLI for cargo build-sbf + - name: Install Solana CLI + run: | + sh -c "$(curl -sSfL https://release.solana.com/stable/install)" + echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH + solana --version + cargo build-sbf --version + + - name: Build ecosystem package + run: | + echo "๐Ÿ”จ Building ${{ needs.setup.outputs.package_name }} v${{ needs.setup.outputs.version }} for publication..." + cargo build --manifest-path "${{ needs.setup.outputs.manifest_path }}" + + - name: Publish to crates.io + run: | + echo "๐Ÿ“ฆ Publishing ${{ needs.setup.outputs.package_name }} v${{ needs.setup.outputs.version }} to crates.io..." + cargo publish --manifest-path "${{ needs.setup.outputs.manifest_path }}" --token "${{ secrets.CARGO_REGISTRY_TOKEN }}" + echo "โœ… Published to crates.io!" + + # Create GitHub release + github-release: + name: Create GitHub Release + runs-on: ubuntu-latest + needs: [setup, publish] + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Create GitHub Release + run: | + ECOSYSTEM_NAME="${{ needs.setup.outputs.ecosystem_name }}" + PACKAGE_NAME="${{ needs.setup.outputs.package_name }}" + VERSION="${{ needs.setup.outputs.version }}" + + # Create release notes + cat > release_notes.md << EOF + # $PACKAGE_NAME v$VERSION + + Pre-built ELF exports for ${ECOSYSTEM_NAME} v${VERSION}. + + ## Installation + + \`\`\`toml + [dependencies] + $PACKAGE_NAME = "$VERSION" + \`\`\` + + ## Usage + + \`\`\`rust + use ${PACKAGE_NAME}::*; + + // Use the ELF constants directly + let program_id = deploy_program(PROGRAM_ELF)?; + \`\`\` + + ## What's Included + + This ecosystem package contains pre-built ELF binaries for ${ECOSYSTEM_NAME} v${VERSION}, matching the exact upstream release. + + - ๐Ÿ”’ **Version-locked** to upstream v${VERSION} + - ๐Ÿš€ **Zero build time** - pre-compiled binaries + - โœ… **Validated** in CI with comprehensive testing + + See the [ecosystem documentation](https://github.com/levicook/elf-magic/blob/main/docs/ecosystem.md) for more details. + EOF + + gh release create "${{ github.ref_name }}" \ + --title "$PACKAGE_NAME v$VERSION" \ + --notes-file release_notes.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..1074009 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ecosystem/solana-spl-token/upstream"] + path = ecosystem/solana-spl-token/upstream + url = https://github.com/solana-program/token diff --git a/Cargo.lock b/Cargo.lock index d9912df..0491aae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -154,6 +154,13 @@ dependencies = [ "toml", ] +[[package]] +name = "elf-magic-solana-spl-token" +version = "3.4.0" +dependencies = [ + "elf-magic", +] + [[package]] name = "equivalent" version = "1.0.2" diff --git a/Cargo.toml b/Cargo.toml index b6fa18f..17798f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,9 @@ +[workspace] +members = [ # + ".", + "ecosystem/solana-spl-token", +] + [package] name = "elf-magic" version = "0.3.1" @@ -10,7 +16,6 @@ documentation = "https://github.com/levicook/elf-magic" readme = "README.md" keywords = ["workspace-automation", "cargo", "elf", "build-tool", "solana"] authors = ["Levi Cook "] -license-file = "LICENSE" [dependencies] anyhow = "1.0" diff --git a/Makefile b/Makefile index d5ca624..22b402f 100644 --- a/Makefile +++ b/Makefile @@ -48,6 +48,27 @@ release-minor: release-major: @./scripts/release.sh major +# Ecosystem package releases (local prep + GitHub automation) +.PHONY: prepare-ecosystem-release prep ecosystem-list +prepare-ecosystem-release: + @if [ -z "$(ECOSYSTEM)" ] || [ -z "$(VERSION)" ]; then \ + echo "โŒ Usage: make prepare-ecosystem-release ECOSYSTEM= VERSION="; \ + echo " Example: make prepare-ecosystem-release ECOSYSTEM=solana-spl-token VERSION=3.4.0"; \ + echo ""; \ + echo "This prepares the ecosystem package locally. Then:"; \ + echo " git push origin main ecosystem//v"; \ + echo " โ†’ Triggers GitHub workflow for validation & publication"; \ + exit 1; \ + fi + @./scripts/prepare-ecosystem-release $(ECOSYSTEM) $(VERSION) + +# Short alias for convenience +prep: prepare-ecosystem-release + +ecosystem-list: + @echo "๐Ÿ“ฆ Available ecosystem packages:" + @find ecosystem -maxdepth 1 -type d -not -name ecosystem | sed 's|ecosystem/| - |' | sort + # ============================================================================= # Testing # ============================================================================= @@ -112,6 +133,11 @@ help: @echo " make release-patch Release patch version (bug fixes)" @echo " make release-validation Complete release validation" @echo "" + @echo "Ecosystem:" + @echo " make ecosystem-list List available ecosystem packages" + @echo " make prepare-ecosystem-release ECOSYSTEM= VERSION=" + @echo " make prep ECOSYSTEM= VERSION= (alias for prepare-ecosystem-release)" + @echo "" @echo "Development:" @echo " make check Check code without building" @echo " make build Build project" diff --git a/README.md b/README.md index db1bbb7..241fafe 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ > Automatic compile-time ELF exports for Solana programs. One-liner integration, zero config, just works. -๐Ÿš€ **New in 0.3**: Intuitive configuration language with `only`/`deny` semantics, comprehensive documentation, and laser-eyes precision targeting! +๐Ÿš€ **New in 0.4**: Ecosystem packages for instant access to popular Solana programs! Plus enhanced configuration with `constants` and `targets` support. + +๐ŸŒŸ **New Ecosystem Packages**: Pre-built ELF exports for popular Solana programs! No more building from source - just add the dependency and go. Stop wrestling with Solana program builds. `elf-magic` automatically discovers all your programs, builds them, and generates clean Rust code so your ELF bytes are always available as constants. @@ -17,7 +19,7 @@ Add to `my-elves/Cargo.toml`: ```toml [build-dependencies] -elf-magic = "0.3" +elf-magic = "0.4" ``` Add to `my-elves/build.rs`: @@ -56,6 +58,16 @@ use my_elves::{TOKEN_MANAGER_ELF, GOVERNANCE_ELF}; let program_id = deploy_program(TOKEN_MANAGER_ELF)?; ``` +## Ecosystem Packages ๐ŸŒŸ + +**NEW**: Pre-built ELF exports for popular Solana programs. Zero build time, just add the dependency. + +| Program | Package | Version | What's Included | +|---------|---------|---------|-----------------| +| SPL Token | `elf-magic-solana-spl-token` | `3.4.0` | Standard + Pinocchio optimized | + +๐Ÿ‘‰ **[Full ecosystem documentation](docs/ecosystem.md)** - Installation, usage examples, roadmap + ## Three Modes for Every Workflow ### ๐Ÿช„ [Magic Mode](docs/modes/magic.md) (Default) @@ -135,11 +147,12 @@ Add to your ELF crate's `Cargo.toml`: ```toml [build-dependencies] -elf-magic = "0.3" +elf-magic = "0.4" ``` ## Documentation +- **๐ŸŒŸ [Ecosystem Packages](docs/ecosystem.md)** - Pre-built ELF exports for popular programs - **๐Ÿช„ [Magic Mode](docs/modes/magic.md)** - Zero config auto-discovery - **๐ŸŽ›๏ธ [Permissive Mode](docs/modes/permissive.md)** - Multi-workspace with exclusions - **๐ŸŽฏ [Laser Eyes Mode](docs/modes/laser-eyes.md)** - Precision targeting diff --git a/docs/ecosystem.md b/docs/ecosystem.md new file mode 100644 index 0000000..0cff7b2 --- /dev/null +++ b/docs/ecosystem.md @@ -0,0 +1,241 @@ +# Ecosystem Packages ๐ŸŒŸ + +**Get popular Solana program binaries instantly - no build tools, no wait time, just add the dependency.** + +Ecosystem packages are pre-compiled ELF exports for popular Solana programs. Instead of building programs from source every time, just add a dependency and get instant access to production-ready program binaries. + +## Why Ecosystem Packages? + +**๐Ÿš€ Zero Build Time** - No more waiting for `cargo build-sbf` +**๐Ÿ”’ Version Locked** - Exact mainnet program versions +**๐Ÿ› ๏ธ No Dependencies** - No Solana CLI tools required +**โœ… CI/CD Friendly** - Fast, reproducible builds + +## Quick Start + +Add an ecosystem package to your project: + +```toml +[dependencies] +elf-magic-solana-spl-token = "3.4.0" +``` + +Use the ELF binaries immediately: + +```rust +use elf_magic_solana_spl_token::{SPL_TOKEN_PROGRAM_ELF, SPL_TOKEN_P_TOKEN_ELF}; + +// Deploy the standard SPL Token program +let token_program_id = deploy_program(SPL_TOKEN_PROGRAM_ELF)?; + +// Or use the Pinocchio-optimized version for better performance +let p_token_program_id = deploy_program(SPL_TOKEN_P_TOKEN_ELF)?; +``` + +That's it! No build configuration, no Solana CLI setup, no waiting. + +## Available Packages + +### SPL Token Program + +```toml +[dependencies] +elf-magic-solana-spl-token = "3.4.0" +``` + +**What's included:** + +- `SPL_TOKEN_PROGRAM_ELF` - Standard SPL Token program +- `SPL_TOKEN_P_TOKEN_ELF` - Pinocchio-optimized version + +**Program ID:** `TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA` +**Upstream:** [solana-program/token](https://github.com/solana-program/token) +**Mainnet Version:** 3.4.0 + +## Usage Examples + +### Testing + +```rust +use elf_magic_solana_spl_token::SPL_TOKEN_PROGRAM_ELF; + +#[test] +fn test_token_operations() { + let program_id = deploy_program_to_test_validator(SPL_TOKEN_PROGRAM_ELF); + // Your token tests here... +} +``` + +### Deployment Scripts + +```rust +use elf_magic_solana_spl_token::SPL_TOKEN_P_TOKEN_ELF; + +fn deploy_optimized_token_program() -> Result { + // Deploy the performance-optimized version + deploy_program(SPL_TOKEN_P_TOKEN_ELF) +} +``` + +### Integration with your elf-magic generated programs + +```rust +// Mix ecosystem packages with your own programs +use my_programs::{MY_DEX_ELF, MY_VAULT_ELF}; // Your elf-magic generated programs +use elf_magic_solana_spl_token::SPL_TOKEN_PROGRAM_ELF; // Ecosystem package + +fn deploy_full_stack() -> Result<(), Error> { + let token_program = deploy_program(SPL_TOKEN_PROGRAM_ELF)?; + let dex_program = deploy_program(MY_DEX_ELF)?; + let vault_program = deploy_program(MY_VAULT_ELF)?; + + // Configure your programs to work together + Ok(()) +} +``` + +## Comparison: Build vs Ecosystem Package + +**Traditional approach:** + +```bash +# Takes 2-5 minutes, requires Solana CLI +git clone https://github.com/solana-program/token +cd token +cargo build-sbf --package spl-token +# Copy .so files around manually... +``` + +**Ecosystem package approach:** + +```toml +# Takes 5 seconds, works anywhere +[dependencies] +elf-magic-solana-spl-token = "3.4.0" +``` + +## FAQ + +**Q: Are ecosystem packages safe?** +A: Yes! They contain identical binaries to what you'd build from source. All builds happen transparently in GitHub CI with full audit logs. + +**Q: How do I know which version to use?** +A: Use the version matching your target deployment. Ecosystem package versions exactly match upstream program versions. + +**Q: What if I need a different version?** +A: Open an issue requesting the specific version. We can publish additional versions as needed. + +**Q: Can I use ecosystem packages with my own programs?** +A: Absolutely! Mix and match ecosystem packages with your own elf-magic generated programs. + +**Q: Do ecosystem packages work without elf-magic?** +A: Yes! Ecosystem packages are standalone crates. You can use them in any Rust project. + +**Q: How are versions kept in sync?** +A: Automated release process tracks upstream git tags and rebuilds when new versions are released. + +## Roadmap + +**Coming soon:** + +- SPL Associated Token program +- Metaplex programs +- Pyth Oracle programs +- Your suggestions! (Open an issue) + +--- + +## Appendix: For Maintainers + +_This section is for people who want to contribute ecosystem packages or understand the release process._ + +### Release Process + +Ecosystem packages use a **local preparation + GitHub automation** workflow: + +```bash +# 1. Prepare release locally (lightweight) +make prepare-ecosystem-release ECOSYSTEM=solana-spl-token VERSION=3.5.0 + +# 2. Push to trigger GitHub workflow (heavy lifting) +git push origin main ecosystem/solana-spl-token/v3.5.0 +``` + +**Local preparation:** + +- Updates git submodule to upstream tag +- Synchronizes Cargo.toml version +- Creates atomic commit + tag + +**GitHub automation:** + +- ELF generation in clean environment +- Comprehensive validation (tests, clippy) +- Transparent crates.io publication +- GitHub release with changelog + +### Adding New Ecosystem Packages + +1. **Create package structure:** + +```bash +mkdir -p ecosystem/your-program-name +``` + +2. **Add git submodule:** + +```bash +git submodule add https://github.com/upstream/repo ecosystem/your-program-name/upstream +``` + +3. **Create Cargo.toml** with elf-magic configuration: + +```toml +[package] +name = "elf-magic-your-program-name" +version = "1.0.0" +edition = "2021" +description = "Pre-built ELF exports for Your Program v1.0.0" +license = "MIT" + +[dependencies] +elf-magic = { path = "../.." } + +[package.metadata.elf-magic] +mode = "laser-eyes" +workspaces = [ + { manifest_path = "./upstream/Cargo.toml", only = ["target:your_program"] }, +] + +[build-dependencies] +elf-magic = { path = "../.." } +``` + +4. **Test the release process:** + +```bash +make prepare-ecosystem-release ECOSYSTEM=your-program-name VERSION=1.0.0 +git push origin main ecosystem/your-program-name/v1.0.0 +``` + +5. **Add to workspace** in root `Cargo.toml`: + +```toml +[workspace] +members = [ + ".", + "ecosystem/solana-spl-token", + "ecosystem/your-program-name", # Add here +] +``` + +### Contributing + +We welcome ecosystem packages for popular Solana programs! Open an issue or PR to discuss. + +**Criteria for inclusion:** + +- Program is widely used in the Solana ecosystem +- Program has stable, tagged releases +- Program builds successfully with `cargo build-sbf` +- Upstream repository is well-maintained diff --git a/docs/modes/magic.md b/docs/modes/magic.md index cb3d365..ce6760e 100644 --- a/docs/modes/magic.md +++ b/docs/modes/magic.md @@ -20,7 +20,7 @@ version = "0.1.0" edition = "2021" [build-dependencies] -elf-magic = "0.3" +elf-magic = "0.4" ``` ### Explicit Magic Mode @@ -34,7 +34,7 @@ edition = "2021" mode = "magic" [build-dependencies] -elf-magic = "0.3" +elf-magic = "0.4" ``` ## How It Works diff --git a/ecosystem/solana-spl-token/Cargo.toml b/ecosystem/solana-spl-token/Cargo.toml new file mode 100644 index 0000000..4db1dd9 --- /dev/null +++ b/ecosystem/solana-spl-token/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "elf-magic-solana-spl-token" +version = "3.4.0" +edition = "2021" +description = "Pre-built ELF exports for Solana SPL Token program v3.4.0" +license = "MIT" +repository = "https://github.com/levicook/elf-magic" +keywords = ["solana", "spl", "token", "elf", "ecosystem"] + +[dependencies] +elf-magic = { path = "../.." } + +[package.metadata.elf-magic] +mode = "laser-eyes" +workspaces = [ + { manifest_path = "./upstream/Cargo.toml", only = [ + "path:*/program/*", + "path:*/p-token/*", + ] }, +] + +[package.metadata.elf-magic.constants] +"./upstream/program/Cargo.toml" = "SPL_TOKEN_PROGRAM_ELF" +"./upstream/p-token/Cargo.toml" = "SPL_TOKEN_P_TOKEN_ELF" + +[package.metadata.upstream] +repository = "https://github.com/solana-program/token" +mainnet_version = "3.4.0" +program_id = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + +[lib] +path = "src/lib.rs" + +[build-dependencies] +elf-magic = { path = "../.." } diff --git a/ecosystem/solana-spl-token/build.rs b/ecosystem/solana-spl-token/build.rs new file mode 100644 index 0000000..3d4d5a4 --- /dev/null +++ b/ecosystem/solana-spl-token/build.rs @@ -0,0 +1,3 @@ +fn main() { + elf_magic::generate().unwrap(); +} diff --git a/ecosystem/solana-spl-token/src/lib.rs b/ecosystem/solana-spl-token/src/lib.rs new file mode 100644 index 0000000..054d4b7 --- /dev/null +++ b/ecosystem/solana-spl-token/src/lib.rs @@ -0,0 +1,22 @@ +// This file is auto-generated by elf-magic +// DO NOT EDIT MANUALLY - Changes will be overwritten +// +// Build Status: +// โœ“ pinocchio_token_program - SUCCESS +// โœ“ spl_token - SUCCESS +// ------------------------------------------------------------ +// Constants +/// ELF binary for the pinocchio_token_program Solana program +pub const SPL_TOKEN_P_TOKEN_ELF: &[u8] = include_bytes!(env!("PINOCCHIO_TOKEN_PROGRAM_ELF_PATH")); + +/// ELF binary for the spl_token Solana program +pub const SPL_TOKEN_PROGRAM_ELF: &[u8] = include_bytes!(env!("SPL_TOKEN_ELF_PATH")); + +/// Get all available Solana program ELF binaries +/// Returns a vector of (program_name, elf_bytes) tuples +pub fn elves() -> Vec<(&'static str, &'static [u8])> { + vec![ + ("pinocchio_token_program", SPL_TOKEN_P_TOKEN_ELF), + ("spl_token", SPL_TOKEN_PROGRAM_ELF), + ] +} diff --git a/ecosystem/solana-spl-token/upstream b/ecosystem/solana-spl-token/upstream new file mode 160000 index 0000000..8921377 --- /dev/null +++ b/ecosystem/solana-spl-token/upstream @@ -0,0 +1 @@ +Subproject commit 8921377ea5cd109d49a888b8ef57f041d6f20ce1 diff --git a/scripts/ecosystem-prepare.sh b/scripts/ecosystem-prepare.sh new file mode 100644 index 0000000..e6df76b --- /dev/null +++ b/scripts/ecosystem-prepare.sh @@ -0,0 +1,94 @@ +#!/bin/bash +set -e + +# Ecosystem package preparation script (local, lightweight) +# Usage: ./scripts/ecosystem-prepare.sh +# Example: ./scripts/ecosystem-prepare.sh solana-spl-token 3.5.0 + +ECOSYSTEM_NAME=${1} +UPSTREAM_VERSION=${2} + +if [[ -z "$ECOSYSTEM_NAME" || -z "$UPSTREAM_VERSION" ]]; then + echo "โŒ Usage: $0 " + echo " Example: $0 solana-spl-token 3.5.0" + exit 1 +fi + +ECOSYSTEM_DIR="ecosystem/${ECOSYSTEM_NAME}" +SUBMODULE_DIR="${ECOSYSTEM_DIR}/upstream" + +# Validate ecosystem package exists +if [[ ! -d "$ECOSYSTEM_DIR" ]]; then + echo "โŒ Ecosystem package not found: $ECOSYSTEM_DIR" + exit 1 +fi + +# Ensure we're in a clean git state +if [[ -n $(git status --porcelain) ]]; then + echo "โŒ Working directory is not clean. Please commit or stash changes first." + git status --short + exit 1 +fi + +echo "๐Ÿš€ Preparing ecosystem package: $ECOSYSTEM_NAME v$UPSTREAM_VERSION" + +# Update git submodule to target version +echo "๐Ÿ“Ž Updating submodule to v$UPSTREAM_VERSION..." +cd "$SUBMODULE_DIR" + +# Fetch latest tags +git fetch --tags + +# Check if the tag exists +if ! git rev-parse "v$UPSTREAM_VERSION" >/dev/null 2>&1; then + echo "โŒ Tag v$UPSTREAM_VERSION not found in upstream repository" + echo "Available tags:" + git tag --sort=-version:refname | head -10 + exit 1 +fi + +# Checkout the specific version +git checkout "v$UPSTREAM_VERSION" +cd - > /dev/null + +# Update Cargo.toml version to match upstream +echo "๐Ÿ“ Updating Cargo.toml version to $UPSTREAM_VERSION..." +CARGO_TOML="${ECOSYSTEM_DIR}/Cargo.toml" + +# Use sed to update version field (cross-platform) +if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS + sed -i '' "s/^version = \".*\"/version = \"$UPSTREAM_VERSION\"/" "$CARGO_TOML" +else + # Linux + sed -i "s/^version = \".*\"/version = \"$UPSTREAM_VERSION\"/" "$CARGO_TOML" +fi + +# Quick local validation that structure is sane +echo "๐Ÿ” Quick local validation..." +if ! cargo metadata --manifest-path "$CARGO_TOML" --format-version 1 >/dev/null 2>&1; then + echo "โŒ Invalid Cargo.toml after version update" + exit 1 +fi + +# Commit the preparation changes +echo "๐Ÿ’พ Committing ecosystem preparation..." +git add "$ECOSYSTEM_DIR" "$SUBMODULE_DIR" +git commit -m "ecosystem: Prepare $ECOSYSTEM_NAME v$UPSTREAM_VERSION + +- Update submodule to upstream v$UPSTREAM_VERSION +- Update package version to match upstream + +Ready for release workflow." + +# Create the ecosystem tag +ECOSYSTEM_TAG="ecosystem/${ECOSYSTEM_NAME}/v${UPSTREAM_VERSION}" +git tag "$ECOSYSTEM_TAG" + +echo "โœ… Ecosystem package prepared!" +echo "๐Ÿท๏ธ Tag: $ECOSYSTEM_TAG" +echo "" +echo "Next steps:" +echo " git push origin main $ECOSYSTEM_TAG" +echo " โ†’ This will trigger the ecosystem release workflow" +echo " โ†’ ELF generation, validation, and publication happen in GitHub" \ No newline at end of file diff --git a/scripts/prepare-ecosystem-release b/scripts/prepare-ecosystem-release new file mode 100755 index 0000000..e6df76b --- /dev/null +++ b/scripts/prepare-ecosystem-release @@ -0,0 +1,94 @@ +#!/bin/bash +set -e + +# Ecosystem package preparation script (local, lightweight) +# Usage: ./scripts/ecosystem-prepare.sh +# Example: ./scripts/ecosystem-prepare.sh solana-spl-token 3.5.0 + +ECOSYSTEM_NAME=${1} +UPSTREAM_VERSION=${2} + +if [[ -z "$ECOSYSTEM_NAME" || -z "$UPSTREAM_VERSION" ]]; then + echo "โŒ Usage: $0 " + echo " Example: $0 solana-spl-token 3.5.0" + exit 1 +fi + +ECOSYSTEM_DIR="ecosystem/${ECOSYSTEM_NAME}" +SUBMODULE_DIR="${ECOSYSTEM_DIR}/upstream" + +# Validate ecosystem package exists +if [[ ! -d "$ECOSYSTEM_DIR" ]]; then + echo "โŒ Ecosystem package not found: $ECOSYSTEM_DIR" + exit 1 +fi + +# Ensure we're in a clean git state +if [[ -n $(git status --porcelain) ]]; then + echo "โŒ Working directory is not clean. Please commit or stash changes first." + git status --short + exit 1 +fi + +echo "๐Ÿš€ Preparing ecosystem package: $ECOSYSTEM_NAME v$UPSTREAM_VERSION" + +# Update git submodule to target version +echo "๐Ÿ“Ž Updating submodule to v$UPSTREAM_VERSION..." +cd "$SUBMODULE_DIR" + +# Fetch latest tags +git fetch --tags + +# Check if the tag exists +if ! git rev-parse "v$UPSTREAM_VERSION" >/dev/null 2>&1; then + echo "โŒ Tag v$UPSTREAM_VERSION not found in upstream repository" + echo "Available tags:" + git tag --sort=-version:refname | head -10 + exit 1 +fi + +# Checkout the specific version +git checkout "v$UPSTREAM_VERSION" +cd - > /dev/null + +# Update Cargo.toml version to match upstream +echo "๐Ÿ“ Updating Cargo.toml version to $UPSTREAM_VERSION..." +CARGO_TOML="${ECOSYSTEM_DIR}/Cargo.toml" + +# Use sed to update version field (cross-platform) +if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS + sed -i '' "s/^version = \".*\"/version = \"$UPSTREAM_VERSION\"/" "$CARGO_TOML" +else + # Linux + sed -i "s/^version = \".*\"/version = \"$UPSTREAM_VERSION\"/" "$CARGO_TOML" +fi + +# Quick local validation that structure is sane +echo "๐Ÿ” Quick local validation..." +if ! cargo metadata --manifest-path "$CARGO_TOML" --format-version 1 >/dev/null 2>&1; then + echo "โŒ Invalid Cargo.toml after version update" + exit 1 +fi + +# Commit the preparation changes +echo "๐Ÿ’พ Committing ecosystem preparation..." +git add "$ECOSYSTEM_DIR" "$SUBMODULE_DIR" +git commit -m "ecosystem: Prepare $ECOSYSTEM_NAME v$UPSTREAM_VERSION + +- Update submodule to upstream v$UPSTREAM_VERSION +- Update package version to match upstream + +Ready for release workflow." + +# Create the ecosystem tag +ECOSYSTEM_TAG="ecosystem/${ECOSYSTEM_NAME}/v${UPSTREAM_VERSION}" +git tag "$ECOSYSTEM_TAG" + +echo "โœ… Ecosystem package prepared!" +echo "๐Ÿท๏ธ Tag: $ECOSYSTEM_TAG" +echo "" +echo "Next steps:" +echo " git push origin main $ECOSYSTEM_TAG" +echo " โ†’ This will trigger the ecosystem release workflow" +echo " โ†’ ELF generation, validation, and publication happen in GitHub" \ No newline at end of file diff --git a/scripts/release.sh b/scripts/release similarity index 100% rename from scripts/release.sh rename to scripts/release diff --git a/src/builder.rs b/src/builder.rs index 4d27729..e6ed846 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,5 +1,5 @@ use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use crate::{ @@ -97,6 +97,21 @@ pub fn build_program(program: &SolanaProgram) -> Result { Ok(program_so_path) } +/// Enable incremental builds for each program +pub fn enable_incremental_builds( + manifest_dir: &Path, + programs: &[SolanaProgram], +) -> Result<(), Error> { + let output_path = manifest_dir.join("src").join("lib.rs"); + println!("cargo:rerun-if-changed={}", output_path.display()); + + for program in programs { + let program_root = program.manifest_path.parent().unwrap(); + println!("cargo:rerun-if-changed={}", program_root.display()); + } + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -107,6 +122,7 @@ mod tests { package_name: "test_package".to_string(), target_name: "test_target".to_string(), manifest_path: PathBuf::from("/workspace/Cargo.toml"), + constant_name: "TEST_TARGET_ELF".to_string(), } } @@ -154,11 +170,13 @@ mod tests { package_name: "pkg1".to_string(), target_name: "target1".to_string(), manifest_path: PathBuf::from("/workspace/Cargo.toml"), + constant_name: "TARGET1_ELF".to_string(), }, SolanaProgram { package_name: "pkg2".to_string(), target_name: "target2".to_string(), manifest_path: PathBuf::from("/workspace/Cargo.toml"), + constant_name: "TARGET2_ELF".to_string(), }, ]; @@ -193,6 +211,7 @@ mod tests { package_name: "my_package".to_string(), target_name: "my-complex-target-name".to_string(), manifest_path: PathBuf::from("/workspace/Cargo.toml"), + constant_name: "MY_COMPLEX_TARGET_NAME_ELF".to_string(), }, ]; diff --git a/src/codegen.rs b/src/codegen.rs index bca4cbb..979e23a 100644 --- a/src/codegen.rs +++ b/src/codegen.rs @@ -1,4 +1,4 @@ -use std::{fs, path::Path}; +use std::{fs, path::Path, process::Command}; use minijinja::{context, Environment}; @@ -40,7 +40,7 @@ pub fn generate(build_result: &BuildResult) -> Result { // Process successful programs for (program, _path) in &build_result.successful { let constant = Some(serde_json::json!({ - "constant_name": program.constant_name(), + "constant_name": program.constant_name, "env_var": program.env_var_name(), "program_name": program.target_name.clone() })); @@ -110,7 +110,33 @@ pub fn save(manifest_dir: &Path, code: &str) -> Result<(), Error> { fs::write(&output_path, code).map_err(|e| { let message = format!("Failed to write lib.rs: {}", e); Error::CodeGeneration(message) - }) + })?; + + // Format the generated code to prevent unexpected changes when users run cargo fmt + format_generated_code(manifest_dir, &output_path); + + Ok(()) +} + +/// Format generated code with cargo fmt, ignoring errors +fn format_generated_code(manifest_dir: &Path, file_path: &Path) { + let result = Command::new("cargo") + .args(["fmt", "--manifest-path"]) + .arg(manifest_dir.join("Cargo.toml")) + .arg("--") + .arg(file_path) + .output(); + + if let Err(e) = result { + eprintln!("Warning: Could not run cargo fmt: {}", e); + } else if let Ok(output) = result { + if !output.status.success() { + eprintln!( + "Warning: Failed to format generated code: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + } } #[cfg(test)] @@ -125,11 +151,13 @@ mod tests { package_name: "package1".to_string(), target_name: "target1".to_string(), manifest_path: PathBuf::from("/path/to/Cargo.toml"), + constant_name: "TARGET1_ELF".to_string(), }, SolanaProgram { package_name: "package2".to_string(), target_name: "target2".to_string(), manifest_path: PathBuf::from("/path/to/other/Cargo.toml"), + constant_name: "TARGET2_ELF".to_string(), }, ] } @@ -154,6 +182,7 @@ mod tests { package_name: "my_package".to_string(), target_name: "my_target".to_string(), manifest_path: PathBuf::from("/path/to/Cargo.toml"), + constant_name: "MY_TARGET_ELF".to_string(), }]; let result = generate(&BuildResult { @@ -212,6 +241,7 @@ mod tests { package_name: "my-special-package".to_string(), target_name: "my_target_name".to_string(), manifest_path: PathBuf::from("/path/to/Cargo.toml"), + constant_name: "MY_TARGET_NAME_ELF".to_string(), }]; let result = generate(&BuildResult { @@ -277,12 +307,14 @@ mod tests { package_name: "good_package".to_string(), target_name: "good_program".to_string(), manifest_path: PathBuf::from("/path/to/good/Cargo.toml"), + constant_name: "GOOD_PROGRAM_ELF".to_string(), }; let failed_program = SolanaProgram { package_name: "bad_package".to_string(), target_name: "bad_program".to_string(), manifest_path: PathBuf::from("/path/to/bad/Cargo.toml"), + constant_name: "BAD_PROGRAM_ELF".to_string(), }; let build_result = BuildResult { @@ -319,18 +351,21 @@ mod tests { package_name: "zebra".to_string(), target_name: "zebra".to_string(), manifest_path: PathBuf::from("/path/to/zebra/Cargo.toml"), + constant_name: "ZEBRA_ELF".to_string(), }; let alpha_program = SolanaProgram { package_name: "alpha".to_string(), target_name: "alpha".to_string(), manifest_path: PathBuf::from("/path/to/alpha/Cargo.toml"), + constant_name: "ALPHA_ELF".to_string(), }; let beta_program = SolanaProgram { package_name: "beta".to_string(), target_name: "beta".to_string(), manifest_path: PathBuf::from("/path/to/beta/Cargo.toml"), + constant_name: "BETA_ELF".to_string(), }; // Mix success and failure to test unified sorting @@ -393,43 +428,48 @@ mod tests { #[test] fn test_alphabetical_sorting_case_sensitive() { let lower_program = SolanaProgram { - package_name: "lower".to_string(), - target_name: "aaa_program".to_string(), - manifest_path: PathBuf::from("/path/to/lower/Cargo.toml"), + package_name: "lowercase".to_string(), + target_name: "lowercase".to_string(), + manifest_path: PathBuf::from("/path/to/lowercase/Cargo.toml"), + constant_name: "LOWERCASE_ELF".to_string(), }; let upper_program = SolanaProgram { - package_name: "upper".to_string(), - target_name: "ZZZ_program".to_string(), - manifest_path: PathBuf::from("/path/to/upper/Cargo.toml"), + package_name: "UPPERCASE".to_string(), + target_name: "UPPERCASE".to_string(), + manifest_path: PathBuf::from("/path/to/UPPERCASE/Cargo.toml"), + constant_name: "UPPERCASE_ELF".to_string(), }; let build_result = BuildResult { successful: vec![ - (upper_program, PathBuf::from("/tmp/ZZZ_program.so")), - (lower_program, PathBuf::from("/tmp/aaa_program.so")), + (upper_program, PathBuf::from("/tmp/UPPERCASE.so")), + (lower_program, PathBuf::from("/tmp/lowercase.so")), ], failed: Vec::new(), }; let result = generate(&build_result).unwrap(); - // Build status should be sorted by target_name: ZZZ_program, aaa_program (uppercase comes first in ASCII) + // Build status should be sorted by target_name: UPPERCASE, lowercase (uppercase comes first in ASCII) let build_status_start = result.find("// Build Status:").unwrap(); let build_status_section = &result[build_status_start..]; - let zzz_pos = build_status_section.find("// โœ“ ZZZ_program").unwrap(); - let aaa_pos = build_status_section.find("// โœ“ aaa_program").unwrap(); + let uppercase_pos = build_status_section.find("// โœ“ UPPERCASE").unwrap(); + let lowercase_pos = build_status_section.find("// โœ“ lowercase").unwrap(); - assert!(zzz_pos < aaa_pos, "ZZZ_program should come before aaa_program (uppercase letters come first in ASCII sort)"); + assert!( + uppercase_pos < lowercase_pos, + "UPPERCASE should come before lowercase (uppercase letters come first in ASCII sort)" + ); // Constants should follow same order - let zzz_const_pos = result.find("pub const ZZZ_PROGRAM_ELF").unwrap(); - let aaa_const_pos = result.find("pub const AAA_PROGRAM_ELF").unwrap(); + let uppercase_const_pos = result.find("pub const UPPERCASE_ELF").unwrap(); + let lowercase_const_pos = result.find("pub const LOWERCASE_ELF").unwrap(); assert!( - zzz_const_pos < aaa_const_pos, - "ZZZ_PROGRAM_ELF should come before AAA_PROGRAM_ELF" + uppercase_const_pos < lowercase_const_pos, + "UPPERCASE_ELF should come before LOWERCASE_ELF" ); } } diff --git a/src/config.rs b/src/config.rs index 59ed1df..06a7c8b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,8 @@ -use std::{fs, path::Path}; +use std::{ + collections::HashMap, + fs, + path::{Path, PathBuf}, +}; use serde::{Deserialize, Serialize}; @@ -16,6 +20,10 @@ pub enum Config { #[serde(rename = "laser-eyes")] LaserEyes { workspaces: Vec, + #[serde(default)] + constants: HashMap, + #[serde(default)] + targets: HashMap, }, #[serde(rename = "permissive")] @@ -23,6 +31,10 @@ pub enum Config { workspaces: Vec, #[serde(default)] global_deny: Vec, + #[serde(default)] + constants: HashMap, + #[serde(default)] + targets: HashMap, }, } @@ -59,6 +71,33 @@ impl Config { None => Ok(Config::Magic), } } + + /// Get the mode name as a string + pub fn mode_name(&self) -> &'static str { + match self { + Config::Magic => "magic", + Config::LaserEyes { .. } => "laser-eyes", + Config::Permissive { .. } => "permissive", + } + } + + /// Get the constants map for this config + pub fn constants(&self) -> HashMap { + match self { + Config::Magic => HashMap::new(), + Config::LaserEyes { constants, .. } => constants.clone(), + Config::Permissive { constants, .. } => constants.clone(), + } + } + + /// Get the targets map for this config + pub fn targets(&self) -> HashMap { + match self { + Config::Magic => HashMap::new(), + Config::LaserEyes { targets, .. } => targets.clone(), + Config::Permissive { targets, .. } => targets.clone(), + } + } } impl Default for Config { @@ -83,13 +122,43 @@ pub struct PermissiveWorkspaceConfig { pub deny: Vec, } +/// Resolve constant override paths to absolute paths based on config file location +pub fn resolve_constants_paths( + constants: &HashMap, + config_file_dir: &Path, +) -> HashMap { + let mut resolved = HashMap::new(); + + for (relative_path, constant_name) in constants { + let absolute_path = config_file_dir.join(relative_path); + resolved.insert(absolute_path, constant_name.clone()); + } + + resolved +} + +/// Resolve target override paths to absolute paths based on config file location +pub fn resolve_targets_paths( + targets: &HashMap, + config_file_dir: &Path, +) -> HashMap { + let mut resolved = HashMap::new(); + + for (relative_path, target_name) in targets { + let absolute_path = config_file_dir.join(relative_path); + resolved.insert(absolute_path, target_name.clone()); + } + + resolved +} + #[cfg(test)] mod tests { use super::*; use std::fs; use tempfile::TempDir; - fn create_temp_manifest(content: &str) -> (TempDir, std::path::PathBuf) { + fn create_temp_manifest(content: &str) -> (TempDir, PathBuf) { let temp_dir = TempDir::new().unwrap(); let manifest_path = temp_dir.path().join("Cargo.toml"); fs::write(&manifest_path, content).unwrap(); @@ -159,6 +228,8 @@ workspaces = [ Config::Permissive { workspaces, global_deny, + constants, + targets, } => { assert_eq!(workspaces.len(), 2); assert_eq!(workspaces[0].manifest_path, "./Cargo.toml"); @@ -166,6 +237,8 @@ workspaces = [ assert_eq!(workspaces[1].manifest_path, "examples/basic/Cargo.toml"); assert_eq!(workspaces[1].deny, vec!["target:test*"]); assert_eq!(global_deny.len(), 0); // No global excludes in this test + assert!(constants.is_empty()); + assert!(targets.is_empty()); } _ => panic!("Expected Permissive mode"), } @@ -182,7 +255,7 @@ edition = "2021" [package.metadata.elf-magic] mode = "permissive" workspaces = [ - { manifest_path = "./Cargo.toml", exclude = ["target:test*", "package:dev*"] } + { manifest_path = "./Cargo.toml", exclude = ["target:test*"] } ] "#; @@ -193,10 +266,15 @@ workspaces = [ Config::Permissive { workspaces, global_deny, + constants, + targets, } => { assert_eq!(workspaces.len(), 1); - assert_eq!(workspaces[0].deny, vec!["target:test*", "package:dev*"]); - assert_eq!(global_deny.len(), 0); // No global excludes in this test + assert_eq!(workspaces[0].manifest_path, "./Cargo.toml"); + assert_eq!(workspaces[0].deny, vec!["target:test*"]); + assert_eq!(global_deny.len(), 0); + assert!(constants.is_empty()); + assert!(targets.is_empty()); } _ => panic!("Expected Permissive mode"), } @@ -205,9 +283,9 @@ workspaces = [ #[test] fn test_load_config_missing_file() { let temp_dir = TempDir::new().unwrap(); - let non_existent = temp_dir.path().join("missing"); + let non_existent_dir = temp_dir.path().join("non_existent"); - let result = Config::load(&non_existent); + let result = Config::load(&non_existent_dir); assert!(result.is_err()); assert!(result .unwrap_err() @@ -217,12 +295,12 @@ workspaces = [ #[test] fn test_load_config_invalid_toml() { - let invalid_toml = r#" + let manifest_content = r#" [package -name = "invalid" +name = "test-package" -- invalid TOML syntax "#; - let (_temp_dir, manifest_dir) = create_temp_manifest(invalid_toml); + let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content); let result = Config::load(&manifest_dir); assert!(result.is_err()); @@ -245,10 +323,8 @@ mode = "invalid-mode" let result = Config::load(&manifest_dir); assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("Invalid elf-magic config")); + let error_message = result.unwrap_err().to_string(); + assert!(error_message.contains("Invalid elf-magic config")); } #[test] @@ -256,7 +332,7 @@ mode = "invalid-mode" let config = Config::default(); match config { Config::Magic => {} - _ => panic!("Default should be Magic mode"), + _ => panic!("Expected Magic mode as default"), } } @@ -282,9 +358,13 @@ workspaces = [ Config::Permissive { workspaces, global_deny, + constants, + targets, } => { assert_eq!(workspaces[0].deny.len(), 0); // Should default to empty assert_eq!(global_deny.len(), 0); // Should default to empty + assert!(constants.is_empty()); + assert!(targets.is_empty()); } _ => panic!("Expected Permissive mode"), } @@ -314,6 +394,8 @@ workspaces = [ Config::Permissive { workspaces, global_deny, + constants, + targets, } => { assert_eq!(workspaces.len(), 2); assert_eq!( @@ -328,6 +410,9 @@ workspaces = [ // Second workspace has local excludes assert_eq!(workspaces[1].manifest_path, "examples/escrow/Cargo.toml"); assert_eq!(workspaces[1].deny, vec!["target:test*"]); + + assert!(constants.is_empty()); + assert!(targets.is_empty()); } _ => panic!("Expected Permissive mode"), } @@ -353,7 +438,11 @@ workspaces = [ let config = Config::load(&manifest_dir).unwrap(); match config { - Config::LaserEyes { workspaces } => { + Config::LaserEyes { + workspaces, + constants, + targets, + } => { assert_eq!(workspaces.len(), 2); // First workspace @@ -369,6 +458,9 @@ workspaces = [ workspaces[1].only, vec!["target:swap*", "package:my-*-program"] ); + + assert!(constants.is_empty()); + assert!(targets.is_empty()); } _ => panic!("Expected LaserEyes mode"), } @@ -393,10 +485,16 @@ workspaces = [ let config = Config::load(&manifest_dir).unwrap(); match config { - Config::LaserEyes { workspaces } => { + Config::LaserEyes { + workspaces, + constants, + targets, + } => { assert_eq!(workspaces.len(), 1); assert_eq!(workspaces[0].manifest_path, "./Cargo.toml"); assert_eq!(workspaces[0].only, vec!["target:my_program"]); + assert!(constants.is_empty()); + assert!(targets.is_empty()); } _ => panic!("Expected LaserEyes mode"), } @@ -421,10 +519,71 @@ workspaces = [ let config = Config::load(&manifest_dir).unwrap(); match config { - Config::LaserEyes { workspaces } => { + Config::LaserEyes { + workspaces, + constants, + targets, + } => { assert_eq!(workspaces.len(), 1); assert_eq!(workspaces[0].manifest_path, "./Cargo.toml"); assert_eq!(workspaces[0].only.len(), 0); + assert!(constants.is_empty()); + assert!(targets.is_empty()); + } + _ => panic!("Expected LaserEyes mode"), + } + } + + #[test] + fn test_load_config_laser_eyes_mode_with_constants_and_targets() { + let manifest_content = r#" +[package] +name = "test-package" +version = "0.1.0" +edition = "2021" + +[package.metadata.elf-magic] +mode = "laser-eyes" +workspaces = [ + { manifest_path = "./upstream/Cargo.toml", only = ["path:*/program/*", "path:*/p-token/*"] } +] +constants = { "upstream/programs/token/program" = "SPL_TOKEN_PROGRAM_ELF", "upstream/programs/token/p-token" = "SPL_TOKEN_P_TOKEN_ELF" } +targets = { "upstream/programs/token/p-token" = "potato" } +"#; + + let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content); + let config = Config::load(&manifest_dir).unwrap(); + + match config { + Config::LaserEyes { + workspaces, + constants, + targets, + } => { + assert_eq!(workspaces.len(), 1); + assert_eq!(workspaces[0].manifest_path, "./upstream/Cargo.toml"); + assert_eq!( + workspaces[0].only, + vec!["path:*/program/*", "path:*/p-token/*"] + ); + + // Check constants + assert_eq!(constants.len(), 2); + assert_eq!( + constants.get("upstream/programs/token/program"), + Some(&"SPL_TOKEN_PROGRAM_ELF".to_string()) + ); + assert_eq!( + constants.get("upstream/programs/token/p-token"), + Some(&"SPL_TOKEN_P_TOKEN_ELF".to_string()) + ); + + // Check targets + assert_eq!(targets.len(), 1); + assert_eq!( + targets.get("upstream/programs/token/p-token"), + Some(&"potato".to_string()) + ); } _ => panic!("Expected LaserEyes mode"), } diff --git a/src/lib.rs b/src/lib.rs index 19c928d..5014234 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,176 +5,44 @@ mod error; mod programs; mod workspace; -use std::{ - env, - path::{Path, PathBuf}, -}; +use std::{env, path::PathBuf}; use crate::{ config::Config, error::Error, - programs::{GenerationResult, SolanaProgram}, + programs::{deduplicate_programs, GenerationResult, SolanaProgram}, }; /// Generate Rust bindings for Solana programs pub fn generate() -> Result { let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR") .map(PathBuf::from) - .map_err(|e| { - let message = format!("CARGO_MANIFEST_DIR not set: {}", e); - Error::WorkspaceDiscovery(message) - })?; - - // Core pipeline: - // 1. load config - // 2. load workspaces - // 3. discover programs across all workspaces - // 4. build included programs across all workspaces (collect successes and failures) - // 5. generate code - one lib.rs from successfully built programs, with build status - // 6. enable incremental builds for all included programs + .map_err(|e| Error::WorkspaceDiscovery(format!("CARGO_MANIFEST_DIR not set: {}", e)))?; + // Clean pipeline: load โ†’ discover โ†’ build โ†’ generate โ†’ save let config = Config::load(&cargo_manifest_dir)?; - - let workspaces = workspace::load_workspaces(&config)?; - + let workspaces = workspace::load_workspaces(&cargo_manifest_dir, &config)?; let discovered_programs = workspaces .iter() .map(|w| w.discover_programs()) .collect::, _>>()?; - // Extract all programs for building and code generation + // Extract and deduplicate included programs let included_programs: Vec = discovered_programs .iter() .flat_map(|w| w.included.iter().cloned()) .collect(); - - // Deduplicate programs by manifest_path to avoid duplicate constants let included_programs = deduplicate_programs(included_programs); + // Build, generate, and save let build_result = builder::build_programs(&included_programs); - - // Generate code from successfully built programs, including build status let code = codegen::generate(&build_result)?; codegen::save(&cargo_manifest_dir, &code)?; - // Enable incremental builds for all programs (successful and failed) - enable_incremental_builds(&cargo_manifest_dir, &included_programs)?; - - let mode = match &config { - Config::LaserEyes { .. } => "laser-eyes".to_string(), - Config::Magic => "magic".to_string(), - Config::Permissive { .. } => "permissive".to_string(), - }; - - Ok(GenerationResult::new(mode, discovered_programs)) -} - -/// Enable incremental builds for each program -fn enable_incremental_builds(manifest_dir: &Path, programs: &[SolanaProgram]) -> Result<(), Error> { - let output_path = manifest_dir.join("src").join("lib.rs"); - println!("cargo:rerun-if-changed={}", output_path.display()); - - for program in programs { - let program_root = program.manifest_path.parent().unwrap(); - println!("cargo:rerun-if-changed={}", program_root.display()); - } - Ok(()) -} - -/// Deduplicate programs by manifest_path to handle cases where multiple workspaces -/// discover the same program (e.g., shared dependencies) -fn deduplicate_programs(programs: Vec) -> Vec { - use std::collections::HashMap; - - let mut seen: HashMap = HashMap::new(); - - for program in programs { - // Use manifest_path as the key, keeping the first occurrence - seen.entry(program.manifest_path.clone()).or_insert(program); - } - - let mut deduplicated: Vec = seen.into_values().collect(); - - // Sort by target_name to ensure consistent alphabetical ordering - deduplicated.sort_by(|a, b| a.target_name.cmp(&b.target_name)); - - deduplicated -} - -#[cfg(test)] -mod tests { - use super::*; - use std::path::PathBuf; - - #[test] - fn test_deduplicate_programs_removes_duplicates_by_manifest_path() { - // Same program appearing twice (realistic scenario from multiple workspaces) - let program1 = SolanaProgram { - package_name: "apl-token".to_string(), - target_name: "apl_token".to_string(), - manifest_path: PathBuf::from("/repo/token/Cargo.toml"), - }; - - let program2 = SolanaProgram { - package_name: "apl-token".to_string(), - target_name: "apl_token".to_string(), - manifest_path: PathBuf::from("/repo/token/Cargo.toml"), // Same path! - }; - - let escrow_program = SolanaProgram { - package_name: "escrow_program".to_string(), - target_name: "escrow_program".to_string(), - manifest_path: PathBuf::from("/repo/examples/escrow/program/Cargo.toml"), - }; - - // Input: 3 programs (with 1 duplicate) - let input_programs = vec![escrow_program.clone(), program1, program2]; - assert_eq!(input_programs.len(), 3); - - // After deduplication: should have 2 unique programs - let deduplicated = deduplicate_programs(input_programs); - assert_eq!( - deduplicated.len(), - 2, - "Should deduplicate to 2 unique programs" - ); - - // Verify we have one of each program type - let apl_token_count = deduplicated - .iter() - .filter(|p| p.target_name == "apl_token") - .count(); - assert_eq!( - apl_token_count, 1, - "Should have exactly 1 apl_token after deduplication" - ); - - let escrow_count = deduplicated - .iter() - .filter(|p| p.target_name == "escrow_program") - .count(); - assert_eq!(escrow_count, 1, "Should have exactly 1 escrow_program"); - } - - #[test] - fn test_deduplicate_programs_preserves_unique_programs() { - let program1 = SolanaProgram { - package_name: "counter".to_string(), - target_name: "counter_program".to_string(), - manifest_path: PathBuf::from("/repo/examples/counter/program/Cargo.toml"), - }; - - let program2 = SolanaProgram { - package_name: "escrow".to_string(), - target_name: "escrow_program".to_string(), - manifest_path: PathBuf::from("/repo/examples/escrow/program/Cargo.toml"), - }; - - // Input: 2 unique programs - let input_programs = vec![program1, program2]; - let deduplicated = deduplicate_programs(input_programs); + builder::enable_incremental_builds(&cargo_manifest_dir, &included_programs)?; - // Should preserve both programs - assert_eq!(deduplicated.len(), 2, "Should preserve all unique programs"); - } + Ok(GenerationResult::new( + config.mode_name().to_string(), + discovered_programs, + )) } diff --git a/src/programs.rs b/src/programs.rs index aaa47df..9e2da0a 100644 --- a/src/programs.rs +++ b/src/programs.rs @@ -1,7 +1,5 @@ -use std::fmt; -use std::path::PathBuf; - use crate::error::Error; +use std::{fmt, path::PathBuf}; /// A confirmed Solana program (has crate-type = ["cdylib"]) #[derive(Clone)] @@ -9,6 +7,7 @@ pub struct SolanaProgram { pub manifest_path: PathBuf, pub package_name: String, pub target_name: String, + pub constant_name: String, } /// Result of building multiple Solana programs @@ -40,11 +39,6 @@ impl SolanaProgram { pub fn env_var_name(&self) -> String { format!("{}_ELF_PATH", self.target_name.to_uppercase()) } - - /// Convert target name to constant name for generated code - pub fn constant_name(&self) -> String { - format!("{}_ELF", self.target_name.to_uppercase()) - } } impl fmt::Debug for SolanaProgram { @@ -53,7 +47,7 @@ impl fmt::Debug for SolanaProgram { .field("target_name", &self.target_name) .field("manifest_path", &self.manifest_path.display()) .field("env_var_name", &self.env_var_name()) - .field("constant_name", &self.constant_name()) + .field("constant_name", &self.constant_name) .finish() } } @@ -147,6 +141,26 @@ impl GenerationResult { } } +/// Deduplicate programs by manifest_path to handle cases where multiple workspaces +/// discover the same program (e.g., shared dependencies) +pub fn deduplicate_programs(programs: Vec) -> Vec { + use std::collections::HashMap; + + let mut seen: HashMap = HashMap::new(); + + for program in programs { + // Use manifest_path as the key, keeping the first occurrence + seen.entry(program.manifest_path.clone()).or_insert(program); + } + + let mut deduplicated: Vec = seen.into_values().collect(); + + // Sort by target_name to ensure consistent alphabetical ordering + deduplicated.sort_by(|a, b| a.target_name.cmp(&b.target_name)); + + deduplicated +} + #[cfg(test)] mod tests { use super::*; @@ -157,6 +171,7 @@ mod tests { package_name: "my-package".to_string(), target_name: "my_target".to_string(), manifest_path: PathBuf::from("/path/to/Cargo.toml"), + constant_name: "MY_TARGET_ELF".to_string(), } } @@ -166,30 +181,96 @@ mod tests { assert_eq!(program.env_var_name(), "MY_TARGET_ELF_PATH"); } - #[test] - fn test_constant_name() { - let program = sample_program(); - assert_eq!(program.constant_name(), "MY_TARGET_ELF"); - } - #[test] fn test_env_var_name_with_hyphens() { let program = SolanaProgram { package_name: "my-package".to_string(), target_name: "my_target_program".to_string(), manifest_path: PathBuf::from("/path/to/Cargo.toml"), + constant_name: "MY_TARGET_PROGRAM_ELF".to_string(), }; assert_eq!(program.env_var_name(), "MY_TARGET_PROGRAM_ELF_PATH"); } #[test] - fn test_constant_name_with_hyphens() { - let program = SolanaProgram { - package_name: "my-package".to_string(), - target_name: "my_target_program".to_string(), - manifest_path: PathBuf::from("/path/to/Cargo.toml"), + fn test_deduplicate_programs_removes_duplicates_by_manifest_path() { + // Same program appearing twice (realistic scenario from multiple workspaces) + let apl_token_duplicate1 = SolanaProgram { + package_name: "apl-token".to_string(), + target_name: "apl_token".to_string(), + manifest_path: PathBuf::from("/repo/token/Cargo.toml"), + constant_name: "APL_TOKEN_ELF".to_string(), }; - assert_eq!(program.constant_name(), "MY_TARGET_PROGRAM_ELF"); + + let apl_token_duplicate2 = SolanaProgram { + package_name: "apl-token".to_string(), + target_name: "apl_token".to_string(), + manifest_path: PathBuf::from("/repo/token/Cargo.toml"), // Same path! + constant_name: "APL_TOKEN_ELF".to_string(), + }; + + let escrow_program = SolanaProgram { + package_name: "escrow_program".to_string(), + target_name: "escrow_program".to_string(), + manifest_path: PathBuf::from("/repo/examples/escrow/program/Cargo.toml"), + constant_name: "ESCROW_PROGRAM_ELF".to_string(), + }; + + // Input: 3 programs (with 1 duplicate) + let input_programs = vec![ + escrow_program.clone(), + apl_token_duplicate1, + apl_token_duplicate2, + ]; + assert_eq!(input_programs.len(), 3); + + // After deduplication: should have 2 unique programs + let deduplicated = deduplicate_programs(input_programs); + assert_eq!( + deduplicated.len(), + 2, + "Should deduplicate to 2 unique programs" + ); + + // Verify we have one of each program type + let apl_token_count = deduplicated + .iter() + .filter(|p| p.target_name == "apl_token") + .count(); + assert_eq!( + apl_token_count, 1, + "Should have exactly 1 apl_token after deduplication" + ); + + let escrow_count = deduplicated + .iter() + .filter(|p| p.target_name == "escrow_program") + .count(); + assert_eq!(escrow_count, 1, "Should have exactly 1 escrow_program"); + } + + #[test] + fn test_deduplicate_programs_preserves_unique_programs() { + let program1 = SolanaProgram { + package_name: "package1".to_string(), + target_name: "target1".to_string(), + manifest_path: PathBuf::from("/workspace/program1/Cargo.toml"), + constant_name: "TARGET1_ELF".to_string(), + }; + + let program2 = SolanaProgram { + package_name: "package2".to_string(), + target_name: "target2".to_string(), + manifest_path: PathBuf::from("/workspace/program2/Cargo.toml"), + constant_name: "TARGET2_ELF".to_string(), + }; + + // Input: 2 unique programs + let input_programs = vec![program1, program2]; + let deduplicated = deduplicate_programs(input_programs); + + // Should preserve both programs + assert_eq!(deduplicated.len(), 2, "Should preserve all unique programs"); } #[test] @@ -198,12 +279,14 @@ mod tests { package_name: "pkg1".to_string(), target_name: "target1".to_string(), manifest_path: PathBuf::from("/workspace/Cargo.toml"), + constant_name: "MY_TARGET_ELF".to_string(), }; let program2 = SolanaProgram { package_name: "pkg2".to_string(), target_name: "target2".to_string(), manifest_path: PathBuf::from("/workspace/Cargo.toml"), + constant_name: "MY_TARGET_ELF".to_string(), }; let discovered = DiscoveredPrograms { @@ -228,12 +311,14 @@ mod tests { package_name: "included".to_string(), target_name: "good_target".to_string(), manifest_path: PathBuf::from("/workspace/Cargo.toml"), + constant_name: "MY_TARGET_ELF".to_string(), }; let excluded_program = SolanaProgram { package_name: "excluded".to_string(), target_name: "bad_target".to_string(), manifest_path: PathBuf::from("/workspace/Cargo.toml"), + constant_name: "MY_TARGET_ELF".to_string(), }; let discovered = DiscoveredPrograms { @@ -272,12 +357,14 @@ mod tests { package_name: "pkg1".to_string(), target_name: "target1".to_string(), manifest_path: PathBuf::from("/workspace/Cargo.toml"), + constant_name: "MY_TARGET_ELF".to_string(), }; let program2 = SolanaProgram { package_name: "pkg2".to_string(), target_name: "target2".to_string(), manifest_path: PathBuf::from("/workspace2/Cargo.toml"), + constant_name: "MY_TARGET_ELF".to_string(), }; let discovered1 = DiscoveredPrograms { diff --git a/src/workspace.rs b/src/workspace.rs index 748a9c6..fb1d2eb 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -1,19 +1,30 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + use cargo_metadata::{CrateType, Metadata, MetadataCommand}; use crate::{ - config::Config, + config::{resolve_constants_paths, resolve_targets_paths, Config}, error::Error, programs::{DiscoveredPrograms, SolanaProgram}, }; /// Load workspaces from config -pub fn load_workspaces(config: &Config) -> Result, Error> { +pub fn load_workspaces(config_file_dir: &Path, config: &Config) -> Result, Error> { + // Get resolved overrides from config + let resolved_constants = resolve_constants_paths(&config.constants(), config_file_dir); + let resolved_targets = resolve_targets_paths(&config.targets(), config_file_dir); + match config { - Config::LaserEyes { workspaces } => { + Config::LaserEyes { workspaces, .. } => { let mut results = Vec::new(); for workspace in workspaces { let metadata = MetadataCommand::new() .manifest_path(&workspace.manifest_path) + .no_deps() + .other_options(vec!["--locked".to_string()]) .exec()?; let manifest_path = workspace.manifest_path.clone(); @@ -22,12 +33,17 @@ pub fn load_workspaces(config: &Config) -> Result, Error> { metadata, manifest_path, filter_mode: FilterMode::Only(workspace.only.clone()), + constants_overrides: resolved_constants.clone(), + targets_overrides: resolved_targets.clone(), }); } Ok(results) } Config::Magic => { - let metadata = MetadataCommand::new().exec()?; + let metadata = MetadataCommand::new() + .no_deps() + .other_options(vec!["--locked".to_string()]) + .exec()?; let manifest_path = metadata .workspace_root @@ -40,16 +56,21 @@ pub fn load_workspaces(config: &Config) -> Result, Error> { metadata, manifest_path, filter_mode: FilterMode::Magic, + constants_overrides: resolved_constants, + targets_overrides: resolved_targets, }]) } Config::Permissive { workspaces, global_deny, + .. } => { let mut results = Vec::new(); for workspace in workspaces { let metadata = MetadataCommand::new() .manifest_path(&workspace.manifest_path) + .no_deps() + .other_options(vec!["--locked".to_string()]) .exec()?; let manifest_path = workspace.manifest_path.clone(); @@ -62,6 +83,8 @@ pub fn load_workspaces(config: &Config) -> Result, Error> { metadata, manifest_path, filter_mode: FilterMode::Deny(merged_denies), + constants_overrides: resolved_constants.clone(), + targets_overrides: resolved_targets.clone(), }); } Ok(results) @@ -85,6 +108,8 @@ pub struct Workspace { pub metadata: Metadata, pub manifest_path: String, pub filter_mode: FilterMode, + pub constants_overrides: HashMap, + pub targets_overrides: HashMap, } impl Workspace { @@ -100,12 +125,26 @@ impl Workspace { continue; } + let manifest_path = package.manifest_path.as_std_path().to_path_buf(); + let base_target_name = target.name.to_string(); + + // Create fully resolved program upfront let program = SolanaProgram { package_name: package.name.to_string(), - target_name: target.name.to_string(), - manifest_path: package.manifest_path.as_std_path().to_path_buf(), + target_name: resolve_target_name( + &base_target_name, + &manifest_path, + &self.targets_overrides, + ), + manifest_path: manifest_path.clone(), + constant_name: resolve_constant_name( + &base_target_name, + &manifest_path, + &self.constants_overrides, + ), }; + // Now filter the fully resolved program match &self.filter_mode { FilterMode::Magic => { // Magic mode: include all programs @@ -142,6 +181,30 @@ impl Workspace { } } +/// Resolve target name using overrides +fn resolve_target_name( + base_target_name: &str, + manifest_path: &Path, + targets_overrides: &HashMap, +) -> String { + targets_overrides + .get(manifest_path) + .cloned() + .unwrap_or_else(|| base_target_name.to_string()) +} + +/// Resolve constant name using overrides +fn resolve_constant_name( + base_target_name: &str, + manifest_path: &Path, + constants_overrides: &HashMap, +) -> String { + constants_overrides + .get(manifest_path) + .cloned() + .unwrap_or_else(|| format!("{}_ELF", base_target_name.to_uppercase())) +} + /// Check if a program should be included in laser-eyes mode (matches only patterns) fn should_only_include_program(program: &SolanaProgram, only_patterns: &[String]) -> bool { only_patterns @@ -189,6 +252,7 @@ mod tests { target_name: target_name.to_string(), package_name: package_name.to_string(), manifest_path: PathBuf::from("/workspace/Cargo.toml"), + constant_name: format!("{}_ELF", target_name.to_uppercase()), } } @@ -238,6 +302,7 @@ mod tests { target_name: "my_target".to_string(), package_name: "my_package".to_string(), manifest_path: PathBuf::from("/workspace/examples/test/Cargo.toml"), + constant_name: "MY_TARGET_ELF".to_string(), }; let deny_patterns = vec!["path:*/examples/*".to_string()]; @@ -250,6 +315,7 @@ mod tests { target_name: "my_target".to_string(), package_name: "my_package".to_string(), manifest_path: PathBuf::from("/workspace/src/Cargo.toml"), + constant_name: "MY_TARGET_ELF".to_string(), }; let deny_patterns = vec!["path:*/examples/*".to_string()]; @@ -297,6 +363,7 @@ mod tests { target_name: "my_target".to_string(), package_name: "my_package".to_string(), manifest_path: PathBuf::from("/workspace/examples/basic/Cargo.toml"), + constant_name: "MY_TARGET_ELF".to_string(), }; assert!(matches_program_pattern(&program, "path:*/examples/*")); @@ -437,6 +504,7 @@ mod tests { target_name: "my_target".to_string(), package_name: "my_package".to_string(), manifest_path: PathBuf::from("/workspace/programs/core/Cargo.toml"), + constant_name: "MY_TARGET_ELF".to_string(), }; let only_patterns = vec!["path:*/programs/core/*".to_string()]; diff --git a/tests/laser_eyes_mode_integration.rs b/tests/laser_eyes_mode_integration.rs index a3231df..a9e3c57 100644 --- a/tests/laser_eyes_mode_integration.rs +++ b/tests/laser_eyes_mode_integration.rs @@ -37,7 +37,7 @@ edition = "2021" let config = Config::load(temp_dir.path()).unwrap(); match config { - Config::LaserEyes { workspaces } => { + Config::LaserEyes { workspaces, .. } => { assert_eq!(workspaces.len(), 2); // First workspace @@ -87,7 +87,7 @@ edition = "2021" let config = Config::load(temp_dir.path()).unwrap(); match config { - Config::LaserEyes { workspaces } => { + Config::LaserEyes { workspaces, .. } => { assert_eq!(workspaces.len(), 1); assert_eq!(workspaces[0].manifest_path, "./Cargo.toml"); assert_eq!(workspaces[0].only, vec!["target:my_program"]); @@ -128,7 +128,7 @@ edition = "2021" let config = Config::load(temp_dir.path()).unwrap(); match config { - Config::LaserEyes { workspaces } => { + Config::LaserEyes { workspaces, .. } => { assert_eq!(workspaces.len(), 1); assert_eq!(workspaces[0].only.len(), 3); assert!(workspaces[0].only.contains(&"target:*_core".to_string())); @@ -175,7 +175,7 @@ edition = "2021" let config = Config::load(temp_dir.path()).unwrap(); match config { - Config::LaserEyes { workspaces } => { + Config::LaserEyes { workspaces, .. } => { assert_eq!(workspaces.len(), 1); assert_eq!(workspaces[0].only.len(), 0); } diff --git a/tests/permissive_mode_integration.rs b/tests/permissive_mode_integration.rs index f2cdde5..f46f3e0 100644 --- a/tests/permissive_mode_integration.rs +++ b/tests/permissive_mode_integration.rs @@ -38,11 +38,15 @@ edition = "2021" Config::Permissive { workspaces, global_deny, + constants, + targets, } => { assert_eq!(workspaces.len(), 1); assert_eq!(workspaces[0].manifest_path, "./Cargo.toml"); assert_eq!(workspaces[0].deny.len(), 0); assert_eq!(global_deny.len(), 0); + assert!(constants.is_empty()); + assert!(targets.is_empty()); } _ => panic!("Expected Permissive config"), } @@ -83,11 +87,15 @@ edition = "2021" Config::Permissive { workspaces, global_deny, + constants, + targets, } => { assert_eq!(workspaces.len(), 1); assert_eq!(workspaces[0].manifest_path, "./Cargo.toml"); assert_eq!(workspaces[0].deny, vec!["target:test*", "package:dev*"]); assert_eq!(global_deny.len(), 0); + assert!(constants.is_empty()); + assert!(targets.is_empty()); } _ => panic!("Expected Permissive config"), } @@ -129,11 +137,15 @@ edition = "2021" Config::Permissive { workspaces, global_deny, + constants, + targets, } => { assert_eq!(workspaces.len(), 1); assert_eq!(workspaces[0].manifest_path, "./Cargo.toml"); assert_eq!(workspaces[0].deny, vec!["target:test*"]); assert_eq!(global_deny, vec!["package:apl-token"]); + assert!(constants.is_empty()); + assert!(targets.is_empty()); } _ => panic!("Expected Permissive config"), } @@ -177,6 +189,8 @@ edition = "2021" Config::Permissive { workspaces, global_deny, + constants, + targets, } => { assert_eq!(workspaces.len(), 3); @@ -194,6 +208,9 @@ edition = "2021" // Third workspace with different local deny patterns assert_eq!(workspaces[2].manifest_path, "tests/Cargo.toml"); assert_eq!(workspaces[2].deny, vec!["path:*/integration/*"]); + + assert!(constants.is_empty()); + assert!(targets.is_empty()); } _ => panic!("Expected Permissive config"), } From 7c0f402842dc54f01afe244ca75a638a6feed3cd Mon Sep 17 00:00:00 2001 From: Levi Cook Date: Thu, 26 Jun 2025 23:31:17 -0600 Subject: [PATCH 2/2] refactor: Streamline ecosystem package release workflow Move from local preparation + CI approach to maintainer-controlled + CI validation: - Replace ecosystem-prepare.sh with manual maintainer workflow - Add comprehensive CI utilities: parse-tag, install-solana, create-github-release - Simplify GitHub workflows by extracting logic to Makefile targets - Remove temp-env testing dependency and unused features - Add integration tests requirement for ecosystem packages - Update documentation with step-by-step release guide - Fix submodule checkout in release workflows This change gives maintainers full control over file changes while keeping robust CI validation and publication automation. --- .github/workflows/ci.yml | 2 + .github/workflows/ecosystem-release.yml | 132 +++--------------- .github/workflows/release.yml | 4 + Cargo.lock | 58 -------- Cargo.toml | 6 - Makefile | 102 ++++++++++---- docs/ecosystem.md | 116 ++++++++++++--- ecosystem/solana-spl-token/Cargo.toml | 5 +- .../solana-spl-token/tests/integration.rs | 31 ++++ scripts/ci/create-github-release | 61 ++++++++ scripts/ci/install-solana | 22 +++ scripts/ci/parse-tag | 33 +++++ scripts/ecosystem-prepare.sh | 94 ------------- scripts/prepare-ecosystem-release | 94 ------------- scripts/update-submodule | 39 ++++++ 15 files changed, 386 insertions(+), 413 deletions(-) create mode 100644 ecosystem/solana-spl-token/tests/integration.rs create mode 100755 scripts/ci/create-github-release create mode 100755 scripts/ci/install-solana create mode 100755 scripts/ci/parse-tag delete mode 100644 scripts/ecosystem-prepare.sh delete mode 100755 scripts/prepare-ecosystem-release create mode 100755 scripts/update-submodule diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48ae382..88fe768 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: recursive # Cache Docker layers for faster subsequent builds - name: Cache Docker layers diff --git a/.github/workflows/ecosystem-release.yml b/.github/workflows/ecosystem-release.yml index e23cddb..b05782b 100644 --- a/.github/workflows/ecosystem-release.yml +++ b/.github/workflows/ecosystem-release.yml @@ -19,35 +19,20 @@ jobs: version: ${{ steps.parse.outputs.version }} manifest_path: ${{ steps.parse.outputs.manifest_path }} steps: + - uses: actions/checkout@v4 - name: Parse ecosystem tag id: parse run: | - # Tag format: ecosystem/solana-spl-token/v3.4.0 - TAG="${GITHUB_REF#refs/tags/}" - echo "Full tag: $TAG" + # Use make to parse the tag + eval $(make parse-ecosystem-tag TAG="${GITHUB_REF#refs/tags/}") - # Extract ecosystem name (solana-spl-token) - ECOSYSTEM_NAME=$(echo "$TAG" | cut -d'/' -f2) + # Set GitHub outputs echo "ecosystem_name=$ECOSYSTEM_NAME" >> $GITHUB_OUTPUT - - # Extract version (3.4.0) - VERSION=$(echo "$TAG" | cut -d'/' -f3 | sed 's/^v//') echo "version=$VERSION" >> $GITHUB_OUTPUT - - # Generate package name (elf-magic-solana-spl-token) - PACKAGE_NAME="elf-magic-$ECOSYSTEM_NAME" echo "package_name=$PACKAGE_NAME" >> $GITHUB_OUTPUT - - # Generate manifest path - MANIFEST_PATH="ecosystem/$ECOSYSTEM_NAME/Cargo.toml" echo "manifest_path=$MANIFEST_PATH" >> $GITHUB_OUTPUT - - echo "Ecosystem: $ECOSYSTEM_NAME" - echo "Package: $PACKAGE_NAME" - echo "Version: $VERSION" - echo "Manifest: $MANIFEST_PATH" - # Validate ecosystem package in Docker environment + # Validate ecosystem package validate: name: Ecosystem Validation runs-on: ubuntu-latest @@ -55,8 +40,8 @@ jobs: steps: - uses: actions/checkout@v4 with: - submodules: recursive # Important: fetch git submodules - + submodules: recursive + # Cache Cargo registry - name: Cache Cargo registry uses: actions/cache@v4 @@ -69,41 +54,13 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - # Install Solana CLI for cargo build-sbf - name: Install Solana CLI - run: | - sh -c "$(curl -sSfL https://release.solana.com/stable/install)" - echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - solana --version - cargo build-sbf --version + run: make install-solana - name: Validate ecosystem package - run: | - echo "๐Ÿ”จ Building ecosystem package: ${{ needs.setup.outputs.package_name }} v${{ needs.setup.outputs.version }}" - cargo build --manifest-path "${{ needs.setup.outputs.manifest_path }}" - - echo "๐Ÿงช Running tests..." - cargo test --manifest-path "${{ needs.setup.outputs.manifest_path }}" - - echo "๐Ÿ“Ž Running clippy..." - cargo clippy --manifest-path "${{ needs.setup.outputs.manifest_path }}" --all-targets -- -D warnings - - echo "๐Ÿ” Validating generated ELF constants..." - GENERATED_LIB="ecosystem/${{ needs.setup.outputs.ecosystem_name }}/src/lib.rs" - if [ ! -f "$GENERATED_LIB" ]; then - echo "โŒ Generated lib.rs not found" - exit 1 - fi - - if ! grep -q "pub const.*_ELF: &\[u8\]" "$GENERATED_LIB"; then - echo "โŒ No ELF constants found in generated code" - exit 1 - fi - - echo "โœ… ELF constants validation passed" - - echo "๐Ÿ“ฆ Publish dry run..." - cargo publish --manifest-path "${{ needs.setup.outputs.manifest_path }}" --dry-run + run: make validate-ecosystem-package MANIFEST_PATH="${{ needs.setup.outputs.manifest_path }}" VERSION="${{ needs.setup.outputs.version }}" + env: + PATH: ${{ env.PATH }}:$HOME/.local/share/solana/install/active_release/bin # Publish to crates.io publish: @@ -118,24 +75,14 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - # Install Solana CLI for cargo build-sbf - name: Install Solana CLI - run: | - sh -c "$(curl -sSfL https://release.solana.com/stable/install)" - echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - solana --version - cargo build-sbf --version - - - name: Build ecosystem package - run: | - echo "๐Ÿ”จ Building ${{ needs.setup.outputs.package_name }} v${{ needs.setup.outputs.version }} for publication..." - cargo build --manifest-path "${{ needs.setup.outputs.manifest_path }}" + run: make install-solana - - name: Publish to crates.io - run: | - echo "๐Ÿ“ฆ Publishing ${{ needs.setup.outputs.package_name }} v${{ needs.setup.outputs.version }} to crates.io..." - cargo publish --manifest-path "${{ needs.setup.outputs.manifest_path }}" --token "${{ secrets.CARGO_REGISTRY_TOKEN }}" - echo "โœ… Published to crates.io!" + - name: Publish ecosystem package + run: make publish-ecosystem-package MANIFEST_PATH="${{ needs.setup.outputs.manifest_path }}" + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + PATH: ${{ env.PATH }}:$HOME/.local/share/solana/install/active_release/bin # Create GitHub release github-release: @@ -149,45 +96,10 @@ jobs: - name: Create GitHub Release run: | - ECOSYSTEM_NAME="${{ needs.setup.outputs.ecosystem_name }}" - PACKAGE_NAME="${{ needs.setup.outputs.package_name }}" - VERSION="${{ needs.setup.outputs.version }}" - - # Create release notes - cat > release_notes.md << EOF - # $PACKAGE_NAME v$VERSION - - Pre-built ELF exports for ${ECOSYSTEM_NAME} v${VERSION}. - - ## Installation - - \`\`\`toml - [dependencies] - $PACKAGE_NAME = "$VERSION" - \`\`\` - - ## Usage - - \`\`\`rust - use ${PACKAGE_NAME}::*; - - // Use the ELF constants directly - let program_id = deploy_program(PROGRAM_ELF)?; - \`\`\` - - ## What's Included - - This ecosystem package contains pre-built ELF binaries for ${ECOSYSTEM_NAME} v${VERSION}, matching the exact upstream release. - - - ๐Ÿ”’ **Version-locked** to upstream v${VERSION} - - ๐Ÿš€ **Zero build time** - pre-compiled binaries - - โœ… **Validated** in CI with comprehensive testing - - See the [ecosystem documentation](https://github.com/levicook/elf-magic/blob/main/docs/ecosystem.md) for more details. - EOF - - gh release create "${{ github.ref_name }}" \ - --title "$PACKAGE_NAME v$VERSION" \ - --notes-file release_notes.md + make create-ecosystem-github-release \ + ECOSYSTEM_NAME="${{ needs.setup.outputs.ecosystem_name }}" \ + PACKAGE_NAME="${{ needs.setup.outputs.package_name }}" \ + VERSION="${{ needs.setup.outputs.version }}" \ + TAG="${GITHUB_REF#refs/tags/}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 366df26..43b4412 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: recursive - run: TAG_VERSION="${GITHUB_REF#refs/tags/v}" make release-validation # Publish to crates.io @@ -24,6 +26,8 @@ jobs: needs: validate steps: - uses: actions/checkout@v4 + with: + submodules: recursive - uses: dtolnay/rust-toolchain@stable - run: make publish env: diff --git a/Cargo.lock b/Cargo.lock index 0491aae..254369d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,7 +148,6 @@ dependencies = [ "minijinja", "serde", "serde_json", - "temp-env", "tempfile", "thiserror 2.0.12", "toml", @@ -401,16 +400,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" -[[package]] -name = "lock_api" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" -dependencies = [ - "autocfg", - "scopeguard", -] - [[package]] name = "log" version = "0.4.27" @@ -456,29 +445,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "parking_lot" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.52.6", -] - [[package]] name = "percent-encoding" version = "2.3.1" @@ -518,15 +484,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "redox_syscall" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" -dependencies = [ - "bitflags", -] - [[package]] name = "rustix" version = "1.0.7" @@ -552,12 +509,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "semver" version = "1.0.26" @@ -669,15 +620,6 @@ dependencies = [ "syn", ] -[[package]] -name = "temp-env" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96374855068f47402c3121c6eed88d29cb1de8f3ab27090e273e420bdabcf050" -dependencies = [ - "parking_lot", -] - [[package]] name = "tempfile" version = "3.20.0" diff --git a/Cargo.toml b/Cargo.toml index 17798f1..90ee60f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,10 +27,4 @@ toml = "0.8" minijinja = "2.10.2" chrono = { version = "0.4", features = ["serde"] } tempfile = "3.0" -temp-env = { version = "0.3", optional = true } cargo_metadata = "0.20" - -[features] -testing = ["dep:temp-env"] - -[dev-dependencies] diff --git a/Makefile b/Makefile index 22b402f..e5f74d8 100644 --- a/Makefile +++ b/Makefile @@ -10,8 +10,8 @@ ci: $(MAKE) test @echo "All binaries clippy:" cargo clippy --all-targets --all-features -- -D warnings - @echo "Publish dry run:" - cargo publish --dry-run --allow-dirty + @echo "Core package publish dry run:" + cargo publish --package elf-magic --dry-run --allow-dirty @echo "โœ… CI passed" # Release validation - comprehensive checks before publishing @@ -33,9 +33,9 @@ release-validation: # Publish to crates.io (requires CARGO_REGISTRY_TOKEN) .PHONY: publish publish: - @echo "๐Ÿ“ฆ Publishing to crates.io..." - cargo publish --token $$CARGO_REGISTRY_TOKEN - @echo "โœ… Published to crates.io" + @echo "๐Ÿ“ฆ Publishing core elf-magic package to crates.io..." + cargo publish --package elf-magic --token $$CARGO_REGISTRY_TOKEN + @echo "โœ… Published elf-magic to crates.io" # Dogfooding - Use our own tool for releases! ๐ŸŽฏ .PHONY: release-patch release-minor release-major @@ -48,27 +48,73 @@ release-minor: release-major: @./scripts/release.sh major -# Ecosystem package releases (local prep + GitHub automation) -.PHONY: prepare-ecosystem-release prep ecosystem-list -prepare-ecosystem-release: - @if [ -z "$(ECOSYSTEM)" ] || [ -z "$(VERSION)" ]; then \ - echo "โŒ Usage: make prepare-ecosystem-release ECOSYSTEM= VERSION="; \ - echo " Example: make prepare-ecosystem-release ECOSYSTEM=solana-spl-token VERSION=3.4.0"; \ - echo ""; \ - echo "This prepares the ecosystem package locally. Then:"; \ - echo " git push origin main ecosystem//v"; \ - echo " โ†’ Triggers GitHub workflow for validation & publication"; \ - exit 1; \ - fi - @./scripts/prepare-ecosystem-release $(ECOSYSTEM) $(VERSION) - -# Short alias for convenience -prep: prepare-ecosystem-release +# Ecosystem package releases (maintainer-controlled + GitHub validation) +.PHONY: ecosystem-list validate-ecosystem-package publish-ecosystem-package parse-ecosystem-tag install-solana create-ecosystem-github-release ecosystem-list: @echo "๐Ÿ“ฆ Available ecosystem packages:" @find ecosystem -maxdepth 1 -type d -not -name ecosystem | sed 's|ecosystem/| - |' | sort +# Validate ecosystem package (used by CI) +validate-ecosystem-package: + @if [ -z "$(MANIFEST_PATH)" ]; then \ + echo "โŒ MANIFEST_PATH required (e.g., ecosystem/solana-spl-token/Cargo.toml)"; \ + exit 1; \ + fi + @if [ -n "$(VERSION)" ]; then \ + echo "๐Ÿ” Verifying tag version matches Cargo.toml..."; \ + CARGO_VERSION=$$(grep '^version = ' "$(MANIFEST_PATH)" | sed 's/version = "\(.*\)"/\1/'); \ + if [ "$(VERSION)" != "$$CARGO_VERSION" ]; then \ + echo "โŒ Tag version $(VERSION) doesn't match Cargo.toml version $$CARGO_VERSION"; \ + exit 1; \ + fi; \ + echo "โœ… Tag version matches Cargo.toml version: $(VERSION)"; \ + fi + @echo "๐Ÿ”จ Building ecosystem package..." + cargo build --manifest-path "$(MANIFEST_PATH)" + @echo "๐Ÿงช Running tests (includes ELF validation)..." + cargo test --manifest-path "$(MANIFEST_PATH)" + @echo "๐Ÿ“Ž Running clippy..." + cargo clippy --manifest-path "$(MANIFEST_PATH)" --all-targets -- -D warnings + @echo "๐Ÿ“ฆ Publish dry run..." + cargo publish --manifest-path "$(MANIFEST_PATH)" --dry-run + @echo "โœ… Ecosystem package validation passed" + +# Publish ecosystem package to crates.io (used by CI) +publish-ecosystem-package: + @if [ -z "$(MANIFEST_PATH)" ] || [ -z "$(CARGO_REGISTRY_TOKEN)" ]; then \ + echo "โŒ MANIFEST_PATH and CARGO_REGISTRY_TOKEN required"; \ + exit 1; \ + fi + @echo "๐Ÿ”จ Building ecosystem package for publication..." + cargo build --manifest-path "$(MANIFEST_PATH)" + @echo "๐Ÿ“ฆ Publishing to crates.io..." + cargo publish --manifest-path "$(MANIFEST_PATH)" --token "$(CARGO_REGISTRY_TOKEN)" + @echo "โœ… Published to crates.io!" + + +# Parse ecosystem tag (used by CI) +parse-ecosystem-tag: + @if [ -z "$(TAG)" ]; then \ + echo "โŒ TAG required"; \ + echo " Example: make parse-ecosystem-tag TAG=ecosystem/solana-spl-token/v3.4.0"; \ + exit 1; \ + fi + @./scripts/ci/parse-tag "$(TAG)" + +# Install Solana CLI (used by CI) +install-solana: + @./scripts/ci/install-solana + +# Create ecosystem GitHub release (used by CI) +create-ecosystem-github-release: + @if [ -z "$(ECOSYSTEM_NAME)" ] || [ -z "$(PACKAGE_NAME)" ] || [ -z "$(VERSION)" ] || [ -z "$(TAG)" ]; then \ + echo "โŒ ECOSYSTEM_NAME, PACKAGE_NAME, VERSION, and TAG required"; \ + echo " Example: make create-ecosystem-github-release ECOSYSTEM_NAME=solana-spl-token PACKAGE_NAME=elf-magic-solana-spl-token VERSION=3.4.0 TAG=ecosystem/solana-spl-token/v3.4.0"; \ + exit 1; \ + fi + @./scripts/ci/create-github-release "$(ECOSYSTEM_NAME)" "$(PACKAGE_NAME)" "$(VERSION)" "$(TAG)" + # ============================================================================= # Testing # ============================================================================= @@ -131,12 +177,18 @@ help: @echo " make release-major Release major version (breaking changes)" @echo " make release-minor Release minor version (new features)" @echo " make release-patch Release patch version (bug fixes)" - @echo " make release-validation Complete release validation" + @echo " make release-validation Complete release validation" @echo "" @echo "Ecosystem:" - @echo " make ecosystem-list List available ecosystem packages" - @echo " make prepare-ecosystem-release ECOSYSTEM= VERSION=" - @echo " make prep ECOSYSTEM= VERSION= (alias for prepare-ecosystem-release)" + @echo " make ecosystem-list List available ecosystem packages" + @echo "" + @echo "Ecosystem CI:" + @echo " make create-ecosystem-github-release ECOSYSTEM_NAME= PACKAGE_NAME= VERSION= TAG=" + @echo " make install-solana Install Solana CLI" + @echo " make parse-ecosystem-tag TAG= Parse ecosystem release tag" + + @echo " make publish-ecosystem-package MANIFEST_PATH= Publish ecosystem package" + @echo " make validate-ecosystem-package MANIFEST_PATH= Validate ecosystem package" @echo "" @echo "Development:" @echo " make check Check code without building" diff --git a/docs/ecosystem.md b/docs/ecosystem.md index 0cff7b2..615b416 100644 --- a/docs/ecosystem.md +++ b/docs/ecosystem.md @@ -151,28 +151,100 @@ _This section is for people who want to contribute ecosystem packages or underst ### Release Process -Ecosystem packages use a **local preparation + GitHub automation** workflow: - -```bash -# 1. Prepare release locally (lightweight) -make prepare-ecosystem-release ECOSYSTEM=solana-spl-token VERSION=3.5.0 - -# 2. Push to trigger GitHub workflow (heavy lifting) -git push origin main ecosystem/solana-spl-token/v3.5.0 -``` - -**Local preparation:** - -- Updates git submodule to upstream tag -- Synchronizes Cargo.toml version -- Creates atomic commit + tag - -**GitHub automation:** - -- ELF generation in clean environment -- Comprehensive validation (tests, clippy) -- Transparent crates.io publication -- GitHub release with changelog +Ecosystem packages use a **maintainer-controlled + GitHub validation** workflow: + +**What maintainers do:** + +- Update git submodule to upstream tag +- Update Cargo.toml version and dependencies +- Create commit and tag +- Push to trigger CI + +**What GitHub CI does:** + +- Validates the package (build, test, clippy) +- Publishes to crates.io (only if validation passes) +- Creates GitHub release with changelog + +**Key principle: Maintainers control all file changes. CI only validates and publishes.** + +### How to Release an Ecosystem Package + +Example: Releasing `solana-spl-token` version `3.5.0` + +1. **Update submodule to target version**: + + ```bash + cd ecosystem/solana-spl-token/upstream + git fetch --tags + git checkout v3.5.0 + + # Verify you're on the exact tag + git describe --exact-match --tags + # Should output: v3.5.0 + + # Ensure no uncommitted changes in submodule + git status --porcelain + # Should be empty + + cd ../../.. + ``` + +2. **Update package version**: + + ```bash + # Edit ecosystem/solana-spl-token/Cargo.toml + version = "3.5.0" + description = "Pre-built ELF exports for Solana SPL Token program v3.5.0" + ``` + +3. **Generate and review the exported constants**: + + ```bash + cd ecosystem/solana-spl-token + cargo build + # Review generated src/lib.rs - verify expected ELF constants are exported + ``` + +4. **Write validation tests** (required): + + ```rust + // Create tests/integration.rs - validation will fail without tests + use elf_magic_solana_spl_token::*; + + #[test] + fn validate_elf_constants() { + assert!(!SPL_TOKEN_PROGRAM_ELF.is_empty()); + assert!(!SPL_TOKEN_P_TOKEN_ELF.is_empty()); + assert_eq!(&SPL_TOKEN_PROGRAM_ELF[0..4], b"\x7fELF"); + assert_eq!(&SPL_TOKEN_P_TOKEN_ELF[0..4], b"\x7fELF"); + } + ``` + +5. **Validate the package**: + + ```bash + make validate-ecosystem-package MANIFEST_PATH=ecosystem/solana-spl-token/Cargo.toml + ``` + +6. **Commit, tag, and push**: + ```bash + # Stage the submodule commit hash first + git add ecosystem/solana-spl-token/upstream + + # Stage other changes + git add ecosystem/solana-spl-token/Cargo.toml ecosystem/solana-spl-token/tests/ + + # Verify what you're committing + git status + git diff --cached + + git commit -m "Release elf-magic-solana-spl-token v3.5.0" + git tag ecosystem/solana-spl-token/v3.5.0 + git push origin main ecosystem/solana-spl-token/v3.5.0 + ``` + +CI will validate, publish to crates.io, and create a GitHub release. ### Adding New Ecosystem Packages diff --git a/ecosystem/solana-spl-token/Cargo.toml b/ecosystem/solana-spl-token/Cargo.toml index 4db1dd9..b401906 100644 --- a/ecosystem/solana-spl-token/Cargo.toml +++ b/ecosystem/solana-spl-token/Cargo.toml @@ -7,9 +7,6 @@ license = "MIT" repository = "https://github.com/levicook/elf-magic" keywords = ["solana", "spl", "token", "elf", "ecosystem"] -[dependencies] -elf-magic = { path = "../.." } - [package.metadata.elf-magic] mode = "laser-eyes" workspaces = [ @@ -32,4 +29,4 @@ program_id = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" path = "src/lib.rs" [build-dependencies] -elf-magic = { path = "../.." } +elf-magic = { path = "../../" } diff --git a/ecosystem/solana-spl-token/tests/integration.rs b/ecosystem/solana-spl-token/tests/integration.rs new file mode 100644 index 0000000..466bc44 --- /dev/null +++ b/ecosystem/solana-spl-token/tests/integration.rs @@ -0,0 +1,31 @@ +#[test] +fn test_ecosystem_package_has_expected_elves() { + let elves = elf_magic_solana_spl_token::elves(); + + // Should have exactly 2 programs: pinocchio_token_program and spl_token + assert_eq!( + elves.len(), + 2, + "Expected exactly 2 ELF binaries in solana-spl-token ecosystem package" + ); + + // Verify the programs are present + let program_names: Vec<&str> = elves.iter().map(|(name, _)| *name).collect(); + assert!( + program_names.contains(&"pinocchio_token_program"), + "Missing pinocchio_token_program" + ); + assert!(program_names.contains(&"spl_token"), "Missing spl_token"); + + // Verify each ELF binary is non-empty + for (name, elf_bytes) in elves { + assert!(!elf_bytes.is_empty(), "ELF binary for {} is empty", name); + // Basic sanity check - ELF files start with magic bytes + assert_eq!( + &elf_bytes[0..4], + b"\x7fELF", + "Invalid ELF magic bytes for {}", + name + ); + } +} diff --git a/scripts/ci/create-github-release b/scripts/ci/create-github-release new file mode 100755 index 0000000..52efd35 --- /dev/null +++ b/scripts/ci/create-github-release @@ -0,0 +1,61 @@ +#!/bin/bash +set -euo pipefail + +# Create GitHub release for ecosystem package +# Usage: ./scripts/create-ecosystem-release.sh + +if [ $# -ne 4 ]; then + echo "Usage: $0 " >&2 + echo "Example: $0 solana-spl-token elf-magic-solana-spl-token 3.4.0 ecosystem/solana-spl-token/v3.4.0" >&2 + exit 1 +fi + +ECOSYSTEM_NAME="$1" +PACKAGE_NAME="$2" +VERSION="$3" +TAG="$4" + +echo "๐Ÿš€ Creating GitHub release for $PACKAGE_NAME v$VERSION..." + +# Create release notes +cat > release_notes.md << EOF +# $PACKAGE_NAME v$VERSION + +Pre-built ELF exports for ${ECOSYSTEM_NAME} v${VERSION}. + +## Installation + +\`\`\`toml +[dependencies] +$PACKAGE_NAME = "$VERSION" +\`\`\` + +## Usage + +\`\`\`rust +use ${PACKAGE_NAME}::*; + +// Use the ELF constants directly +let program_id = deploy_program(PROGRAM_ELF)?; +\`\`\` + +## What's Included + +This ecosystem package contains pre-built ELF binaries for ${ECOSYSTEM_NAME} v${VERSION}, matching the exact upstream release. + +- ๐Ÿ”’ **Version-locked** to upstream v${VERSION} +- ๐Ÿš€ **Zero build time** - pre-compiled binaries +- โœ… **Validated** in CI with comprehensive testing + +See the [ecosystem documentation](https://github.com/levicook/elf-magic/blob/main/docs/ecosystem.md) for more details. +EOF + +echo "๐Ÿ“ Generated release notes:" +cat release_notes.md + +echo "๐ŸŽฏ Creating GitHub release..." +gh release create "$TAG" \ + --title "$PACKAGE_NAME v$VERSION" \ + --notes-file release_notes.md + +echo "โœ… GitHub release created for $TAG" \ No newline at end of file diff --git a/scripts/ci/install-solana b/scripts/ci/install-solana new file mode 100755 index 0000000..58a9f10 --- /dev/null +++ b/scripts/ci/install-solana @@ -0,0 +1,22 @@ +#!/bin/bash +set -euo pipefail + +# Install Solana CLI for cargo build-sbf +echo "๐Ÿ”ง Installing Solana CLI..." + +if command -v solana >/dev/null 2>&1; then + echo "โœ… Solana CLI already installed:" + solana --version + cargo build-sbf --version + exit 0 +fi + +# Install Solana CLI +sh -c "$(curl -sSfL https://release.solana.com/stable/install)" + +# Add to PATH for current session +export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH" + +echo "โœ… Solana CLI installed:" +solana --version +cargo build-sbf --version \ No newline at end of file diff --git a/scripts/ci/parse-tag b/scripts/ci/parse-tag new file mode 100755 index 0000000..b75fa76 --- /dev/null +++ b/scripts/ci/parse-tag @@ -0,0 +1,33 @@ +#!/bin/bash +set -euo pipefail + +# Parse ecosystem tag format: ecosystem/solana-spl-token/v3.4.0 +# Usage: ./scripts/parse-ecosystem-tag.sh + +if [ $# -ne 1 ]; then + echo "Usage: $0 " >&2 + echo "Example: $0 ecosystem/solana-spl-token/v3.4.0" >&2 + exit 1 +fi + +TAG="$1" +echo "Full tag: $TAG" >&2 + +# Extract ecosystem name (solana-spl-token) +ECOSYSTEM_NAME=$(echo "$TAG" | cut -d'/' -f2) + +# Extract version (3.4.0) +VERSION=$(echo "$TAG" | cut -d'/' -f3 | sed 's/^v//') + +# Generate package name (elf-magic-solana-spl-token) +PACKAGE_NAME="elf-magic-$ECOSYSTEM_NAME" + +# Generate manifest path +MANIFEST_PATH="ecosystem/$ECOSYSTEM_NAME/Cargo.toml" + +echo "ECOSYSTEM_NAME=$ECOSYSTEM_NAME" +echo "VERSION=$VERSION" +echo "PACKAGE_NAME=$PACKAGE_NAME" +echo "MANIFEST_PATH=$MANIFEST_PATH" + +echo "Parsed - Ecosystem: $ECOSYSTEM_NAME, Package: $PACKAGE_NAME, Version: $VERSION, Manifest: $MANIFEST_PATH" >&2 \ No newline at end of file diff --git a/scripts/ecosystem-prepare.sh b/scripts/ecosystem-prepare.sh deleted file mode 100644 index e6df76b..0000000 --- a/scripts/ecosystem-prepare.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/bin/bash -set -e - -# Ecosystem package preparation script (local, lightweight) -# Usage: ./scripts/ecosystem-prepare.sh -# Example: ./scripts/ecosystem-prepare.sh solana-spl-token 3.5.0 - -ECOSYSTEM_NAME=${1} -UPSTREAM_VERSION=${2} - -if [[ -z "$ECOSYSTEM_NAME" || -z "$UPSTREAM_VERSION" ]]; then - echo "โŒ Usage: $0 " - echo " Example: $0 solana-spl-token 3.5.0" - exit 1 -fi - -ECOSYSTEM_DIR="ecosystem/${ECOSYSTEM_NAME}" -SUBMODULE_DIR="${ECOSYSTEM_DIR}/upstream" - -# Validate ecosystem package exists -if [[ ! -d "$ECOSYSTEM_DIR" ]]; then - echo "โŒ Ecosystem package not found: $ECOSYSTEM_DIR" - exit 1 -fi - -# Ensure we're in a clean git state -if [[ -n $(git status --porcelain) ]]; then - echo "โŒ Working directory is not clean. Please commit or stash changes first." - git status --short - exit 1 -fi - -echo "๐Ÿš€ Preparing ecosystem package: $ECOSYSTEM_NAME v$UPSTREAM_VERSION" - -# Update git submodule to target version -echo "๐Ÿ“Ž Updating submodule to v$UPSTREAM_VERSION..." -cd "$SUBMODULE_DIR" - -# Fetch latest tags -git fetch --tags - -# Check if the tag exists -if ! git rev-parse "v$UPSTREAM_VERSION" >/dev/null 2>&1; then - echo "โŒ Tag v$UPSTREAM_VERSION not found in upstream repository" - echo "Available tags:" - git tag --sort=-version:refname | head -10 - exit 1 -fi - -# Checkout the specific version -git checkout "v$UPSTREAM_VERSION" -cd - > /dev/null - -# Update Cargo.toml version to match upstream -echo "๐Ÿ“ Updating Cargo.toml version to $UPSTREAM_VERSION..." -CARGO_TOML="${ECOSYSTEM_DIR}/Cargo.toml" - -# Use sed to update version field (cross-platform) -if [[ "$OSTYPE" == "darwin"* ]]; then - # macOS - sed -i '' "s/^version = \".*\"/version = \"$UPSTREAM_VERSION\"/" "$CARGO_TOML" -else - # Linux - sed -i "s/^version = \".*\"/version = \"$UPSTREAM_VERSION\"/" "$CARGO_TOML" -fi - -# Quick local validation that structure is sane -echo "๐Ÿ” Quick local validation..." -if ! cargo metadata --manifest-path "$CARGO_TOML" --format-version 1 >/dev/null 2>&1; then - echo "โŒ Invalid Cargo.toml after version update" - exit 1 -fi - -# Commit the preparation changes -echo "๐Ÿ’พ Committing ecosystem preparation..." -git add "$ECOSYSTEM_DIR" "$SUBMODULE_DIR" -git commit -m "ecosystem: Prepare $ECOSYSTEM_NAME v$UPSTREAM_VERSION - -- Update submodule to upstream v$UPSTREAM_VERSION -- Update package version to match upstream - -Ready for release workflow." - -# Create the ecosystem tag -ECOSYSTEM_TAG="ecosystem/${ECOSYSTEM_NAME}/v${UPSTREAM_VERSION}" -git tag "$ECOSYSTEM_TAG" - -echo "โœ… Ecosystem package prepared!" -echo "๐Ÿท๏ธ Tag: $ECOSYSTEM_TAG" -echo "" -echo "Next steps:" -echo " git push origin main $ECOSYSTEM_TAG" -echo " โ†’ This will trigger the ecosystem release workflow" -echo " โ†’ ELF generation, validation, and publication happen in GitHub" \ No newline at end of file diff --git a/scripts/prepare-ecosystem-release b/scripts/prepare-ecosystem-release deleted file mode 100755 index e6df76b..0000000 --- a/scripts/prepare-ecosystem-release +++ /dev/null @@ -1,94 +0,0 @@ -#!/bin/bash -set -e - -# Ecosystem package preparation script (local, lightweight) -# Usage: ./scripts/ecosystem-prepare.sh -# Example: ./scripts/ecosystem-prepare.sh solana-spl-token 3.5.0 - -ECOSYSTEM_NAME=${1} -UPSTREAM_VERSION=${2} - -if [[ -z "$ECOSYSTEM_NAME" || -z "$UPSTREAM_VERSION" ]]; then - echo "โŒ Usage: $0 " - echo " Example: $0 solana-spl-token 3.5.0" - exit 1 -fi - -ECOSYSTEM_DIR="ecosystem/${ECOSYSTEM_NAME}" -SUBMODULE_DIR="${ECOSYSTEM_DIR}/upstream" - -# Validate ecosystem package exists -if [[ ! -d "$ECOSYSTEM_DIR" ]]; then - echo "โŒ Ecosystem package not found: $ECOSYSTEM_DIR" - exit 1 -fi - -# Ensure we're in a clean git state -if [[ -n $(git status --porcelain) ]]; then - echo "โŒ Working directory is not clean. Please commit or stash changes first." - git status --short - exit 1 -fi - -echo "๐Ÿš€ Preparing ecosystem package: $ECOSYSTEM_NAME v$UPSTREAM_VERSION" - -# Update git submodule to target version -echo "๐Ÿ“Ž Updating submodule to v$UPSTREAM_VERSION..." -cd "$SUBMODULE_DIR" - -# Fetch latest tags -git fetch --tags - -# Check if the tag exists -if ! git rev-parse "v$UPSTREAM_VERSION" >/dev/null 2>&1; then - echo "โŒ Tag v$UPSTREAM_VERSION not found in upstream repository" - echo "Available tags:" - git tag --sort=-version:refname | head -10 - exit 1 -fi - -# Checkout the specific version -git checkout "v$UPSTREAM_VERSION" -cd - > /dev/null - -# Update Cargo.toml version to match upstream -echo "๐Ÿ“ Updating Cargo.toml version to $UPSTREAM_VERSION..." -CARGO_TOML="${ECOSYSTEM_DIR}/Cargo.toml" - -# Use sed to update version field (cross-platform) -if [[ "$OSTYPE" == "darwin"* ]]; then - # macOS - sed -i '' "s/^version = \".*\"/version = \"$UPSTREAM_VERSION\"/" "$CARGO_TOML" -else - # Linux - sed -i "s/^version = \".*\"/version = \"$UPSTREAM_VERSION\"/" "$CARGO_TOML" -fi - -# Quick local validation that structure is sane -echo "๐Ÿ” Quick local validation..." -if ! cargo metadata --manifest-path "$CARGO_TOML" --format-version 1 >/dev/null 2>&1; then - echo "โŒ Invalid Cargo.toml after version update" - exit 1 -fi - -# Commit the preparation changes -echo "๐Ÿ’พ Committing ecosystem preparation..." -git add "$ECOSYSTEM_DIR" "$SUBMODULE_DIR" -git commit -m "ecosystem: Prepare $ECOSYSTEM_NAME v$UPSTREAM_VERSION - -- Update submodule to upstream v$UPSTREAM_VERSION -- Update package version to match upstream - -Ready for release workflow." - -# Create the ecosystem tag -ECOSYSTEM_TAG="ecosystem/${ECOSYSTEM_NAME}/v${UPSTREAM_VERSION}" -git tag "$ECOSYSTEM_TAG" - -echo "โœ… Ecosystem package prepared!" -echo "๐Ÿท๏ธ Tag: $ECOSYSTEM_TAG" -echo "" -echo "Next steps:" -echo " git push origin main $ECOSYSTEM_TAG" -echo " โ†’ This will trigger the ecosystem release workflow" -echo " โ†’ ELF generation, validation, and publication happen in GitHub" \ No newline at end of file diff --git a/scripts/update-submodule b/scripts/update-submodule new file mode 100755 index 0000000..937888e --- /dev/null +++ b/scripts/update-submodule @@ -0,0 +1,39 @@ +#!/bin/bash +set -euo pipefail + +# Update git submodule to specific version +# Usage: ./scripts/update-submodule + +if [ $# -ne 2 ]; then + echo "Usage: $0 " >&2 + echo "Example: $0 ecosystem/solana-spl-token/upstream 3.4.0" >&2 + exit 1 +fi + +SUBMODULE_PATH="$1" +VERSION="$2" + +if [ ! -d "$SUBMODULE_PATH" ]; then + echo "โŒ Submodule not found: $SUBMODULE_PATH" >&2 + exit 1 +fi + +echo "๐Ÿ“Ž Updating submodule $SUBMODULE_PATH to v$VERSION..." + +cd "$SUBMODULE_PATH" + +# Fetch latest tags +git fetch --tags + +# Check if the tag exists +if ! git rev-parse "v$VERSION" >/dev/null 2>&1; then + echo "โŒ Tag v$VERSION not found in submodule repository" >&2 + echo "Available tags:" >&2 + git tag --sort=-version:refname | head -10 >&2 + exit 1 +fi + +# Checkout the specific version +git checkout "v$VERSION" + +echo "โœ… Submodule updated to v$VERSION"