A Rust CLI for Pinocchio programs. It scaffolds a project, builds and deploys it, keeps program IDs in sync, generates an IDL and a standalone Rust client from it, and lints for Solana-specific safety issues. Sensible defaults, no required configuration.
Installation
# Latest from GitHub
# From source
&&
Quick start
Commands
| Command | Description |
|---|---|
pinoc init <name> |
Create a new project |
pinoc build |
Build the program and regenerate the IDL |
pinoc test |
Build the program, then run tests |
pinoc check |
Lint for Solana-specific safety issues (account, CPI, zero-copy) |
pinoc deploy |
Deploy to a cluster |
pinoc clean |
Clean build artifacts (keypairs preserved) |
pinoc add <package> |
Add a Pinocchio package |
pinoc search [query] |
Search packages |
pinoc keys list |
List program keypairs |
pinoc keys sync |
Sync the program ID in source with its keypair |
pinoc idl |
Regenerate the IDL JSON |
pinoc client generate |
Generate a Rust or TypeScript client from the IDL |
pinoc config init |
Create a Pinoc.toml for the project |
Common options:
pinoc init <name> --with-example: scaffold a worked PDA-account example instead of a no-op programpinoc init <name> --no-git: skip git initializationpinoc deploy --cluster <cluster> --wallet <path>: override deployment settingspinoc build --program-id <ADDRESS>: set the IDL program address for programs that don't calldeclare_id!pinoc build --features <FEATURES>/pinoc test --features <FEATURES>: activate cargo features, passed tocargo build-sbfandcargo test(repeatable or comma-separated, like cargo's own flag)pinoc build --arch <v0|v1|v2|v3|v4>/pinoc test --arch <..>: the SBPF version to build for, passed tocargo build-sbf(see SBPF version)pinoc test --build-features <FEATURES>: features for the pre-test SBF build when they differ from the test features (""for none)pinoc test --no-build: skip the SBF build and test against the existingtarget/deploy/*.sopinoc test -- <CARGO_ARGS>: everything after--goes tocargo test(pinoc test -- --test client,pinoc test -- my_test -- --nocapture)pinoc client generate --language ts: generate a TypeScript client (codama generator) intoclients/tspinoc clean --no-preserve: clean everything, including keypairs
Testing
pinoc test runs cargo build-sbf before cargo test, because SVM tests (mollusk-svm, litesvm, solana-program-test) load target/deploy/*.so, which cargo test does not build. Without the build step, tests silently run against the last-built binary.
--features applies to both steps, except for any feature that enables no-entrypoint (such as the scaffold's test-default), which is left out of the build: no-entrypoint compiles the program out of the SBF artifact. When the build and the tests need different features, set the build's separately:
--build-features is used as given. Pass --no-build to skip the build when iterating on tests that don't load the .so.
Arguments after -- are handed to cargo test unchanged, after the features, so one target or one test can be run without leaving pinoc test:
What --no-build tests
pinoc build and pinoc test write target/deploy/<name>.build.json beside the artifact they build: its features, its --arch, and its length and hash. pinoc test --no-build reads it and stops if the artifact was built with other features or another arch than this run would have used, because tests run against it fail in ways that read as bugs in the program:
Error: target/deploy/prog.so was built with --features devnet, but this run would build it with no features. Drop --no-build to rebuild it, or pass --build-features "devnet" if that is the artifact to test.
With --no-build, --build-features and --arch say which artifact is expected. If there is no record, or the artifact is not the one the record describes (built by cargo build-sbf directly, say), pinoc says it cannot tell what the artifact is and runs the tests.
Entrypoint check
pinoc build, pinoc test, and pinoc deploy refuse an SBF artifact with no entrypoint (an ELF entry address outside its executable code, the same rule the SBF loader enforces), which is what no-entrypoint produces: it builds fine but contains no program, so no test can load it and a deploy would pay rent for an unusable program. The error names the cause (a build feature, the default feature, or a program with no entrypoint). pinoc build and pinoc test delete the bad artifact; pinoc deploy checks whatever it is about to upload, however it was built. Unlike pinoc test, pinoc build never drops a feature on its own: its features describe the artifact you asked for.
SBPF version
cargo build-sbf builds for the SBPF version given by --arch, and for its own default when the flag is absent (v0 in cargo-build-sbf 4.3). pinoc build and pinoc test pass --arch only when one is set, with the flag or in Pinoc.toml:
[]
= "v3"
The flag overrides the file. Setting it in the file keeps pinoc build and pinoc test on the same version; with the flag alone, a pinoc test run without it rebuilds the artifact for the toolchain's default.
Which versions a cluster deploys is decided by two feature gates: one enables SBPFv3, and SIMD-0500 disables deployment of v0, v1 and v2. solana-test-validator 4.3 starts with both active, so it deploys only v3. Before uploading, pinoc deploy reads the artifact's version from its ELF header and asks the cluster for both gates (solana feature status), and stops if the cluster would reject the artifact, naming the --arch to rebuild with. If the cluster cannot be asked, the deploy goes ahead unchecked.
Project structure
pinoc init produces a blank, buildable program with a single no-op instruction:
my_app/
├── Cargo.toml
├── Pinoc.toml # deployment configuration
├── src/lib.rs
└── target/deploy/my_app-keypair.json
A full PDA-account creation example, annotated so pinoc idl works out of the box:
my_app/
├── src/
│ ├── lib.rs
│ ├── entrypoint.rs
│ ├── errors.rs
│ ├── instructions/{mod.rs, initialize.rs}
│ └── states/{mod.rs, state.rs, utils.rs}
├── tests/tests.rs
└── target/deploy/my_app-keypair.json
Configuration
Pinoc.toml holds deployment defaults and is optional:
[]
= "localhost"
= "~/.config/solana/id.json"
Without it, pinoc deploy falls back to solana config get; --cluster/--wallet always override. Create one on demand with pinoc config init.
Key management
keys sync finds the program's own ID declaration anywhere under src/ (either declare_id! or a const ID) and updates it in place.
IDL and client generation
pinoc build regenerates the IDL at target/idl/ on every build, and pinoc client generate renders a standalone Rust client crate from it, or a TypeScript client with --language ts. Both understand shank programs and programs using native Codama derive macros.
- IDL generation (files produced, generator selection, error handling, the zero-copy padding lint): src/idl/README.md
- Client generation (the shank and Codama generators, CPI variants,
fetch_*helpers, output paths): src/client_gen/README.md
Linting
pinoc check statically lints a program for Solana-specific safety issues that rustc, clippy, and rust-analyzer do not model: account ownership and signer checks, cross-program invocation safety, and zero-copy memory layout. Configurable severity, inline // pinoc:allow(CODE) suppression, and --json output for CI. A run that finds no handlers to analyse says so (NO-HANDLERS) instead of reporting a clean result.
- Lint codes, configuration, and suppression: src/check/README.md
Prerequisites
- Rust 1.70+
- Solana CLI
- Node.js/npm (only for the Codama client generator)
Contributing
Fork, branch, make your change with tests, and open a pull request.
&& &&
License
Apache 2.0. See LICENSE.
Support
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Pinocchio: anza-xyz/pinocchio
Acknowledgements
Pinoc began as a fork of solana-chio by Arjun.