diff --git a/CHANGELOG.md b/CHANGELOG.md index 48c0cb6c..e14869d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,6 @@ incremented for features. ## [Unreleased] - ### Fixes * ts: Change commitment message `recent` to `processed` and `max` to `finalized` ([#1128](https://github.com/project-serum/anchor/pull/1128)) @@ -26,6 +25,7 @@ incremented for features. * lang: Handle arrays with const as length ([#968](https://github.com/project-serum/anchor/pull/968)). * ts: Add optional commitment argument to `fetch` and `fetchMultiple` ([#1171](https://github.com/project-serum/anchor/pull/1171)). * lang: Implement `AsRef` for `Account<'a, T>`([#1173](https://github.com/project-serum/anchor/pull/1173)) +* cli: Add `anchor expand` command which wraps around `cargo expand` ([#1160](https://github.com/project-serum/anchor/pull/1160)) ### Breaking diff --git a/Cargo.lock b/Cargo.lock index 5bc25af1..a3128b5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,6 +152,7 @@ dependencies = [ "anchor-syn", "anyhow", "cargo_toml", + "chrono", "clap 3.0.0-beta.4", "dirs", "flate2", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index e1f652b1..25b8c292 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -38,3 +38,4 @@ tokio = "1.0" pathdiff = "0.2.0" cargo_toml = "0.9.2" walkdir = "2" +chrono = "0.4.19" diff --git a/cli/src/lib.rs b/cli/src/lib.rs index c77104e1..19d8a275 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -33,6 +33,7 @@ use solana_sdk::sysvar; use solana_sdk::transaction::Transaction; use std::collections::BTreeMap; use std::collections::HashMap; +use std::ffi::OsString; use std::fs::{self, File}; use std::io::prelude::*; use std::path::{Path, PathBuf}; @@ -97,6 +98,25 @@ pub enum Command { )] cargo_args: Vec, }, + /// Expands macros (wrapper around cargo expand) + /// + /// Use it in a program folder to expand program + /// + /// Use it in a workspace but outside a program + /// folder to expand the entire workspace + Expand { + /// Expand only this program + #[clap(short, long)] + program_name: Option, + /// Arguments to pass to the underlying `cargo expand` command + #[clap( + required = false, + takes_value = true, + multiple_values = true, + last = true + )] + cargo_args: Vec, + }, /// Verifies the on-chain bytecode matches the locally compiled artifact. /// Run this command inside a program subdirectory, i.e., in the dir /// containing the program's Cargo.toml. @@ -370,6 +390,10 @@ pub fn entry(opts: Opts) -> Result<()> { cargo_args, ), Command::Deploy { program_name } => deploy(&opts.cfg_override, program_name), + Command::Expand { + program_name, + cargo_args, + } => expand(&opts.cfg_override, program_name, &cargo_args), Command::Upgrade { program_id, program_filepath, @@ -559,6 +583,102 @@ fn new_program(name: &str) -> Result<()> { Ok(()) } +pub fn expand( + cfg_override: &ConfigOverride, + program_name: Option, + cargo_args: &[String], +) -> Result<()> { + // Change to the workspace member directory, if needed. + if let Some(program_name) = program_name.as_ref() { + cd_member(cfg_override, program_name)?; + } + + let workspace_cfg = Config::discover(cfg_override)?.expect("Not in workspace."); + let cfg_parent = workspace_cfg.path().parent().expect("Invalid Anchor.toml"); + let cargo = Manifest::discover()?; + + let expansions_path = cfg_parent.join(".anchor/expanded-macros"); + fs::create_dir_all(&expansions_path)?; + + match cargo { + // No Cargo.toml found, expand entire workspace + None => expand_all(&workspace_cfg, expansions_path, cargo_args), + // Cargo.toml is at root of workspace, expand entire workspace + Some(cargo) if cargo.path().parent() == workspace_cfg.path().parent() => { + expand_all(&workspace_cfg, expansions_path, cargo_args) + } + // Reaching this arm means Cargo.toml belongs to a single package. Expand it. + Some(cargo) => expand_program( + // If we found Cargo.toml, it must be in a directory so unwrap is safe + cargo.path().parent().unwrap().to_path_buf(), + expansions_path, + cargo_args, + ), + } +} + +fn expand_all( + workspace_cfg: &WithPath, + expansions_path: PathBuf, + cargo_args: &[String], +) -> Result<()> { + let cur_dir = std::env::current_dir()?; + for p in workspace_cfg.get_program_list()? { + expand_program(p, expansions_path.clone(), cargo_args)?; + } + std::env::set_current_dir(cur_dir)?; + Ok(()) +} + +fn expand_program( + program_path: PathBuf, + expansions_path: PathBuf, + cargo_args: &[String], +) -> Result<()> { + let cargo = Manifest::from_path(program_path.join("Cargo.toml")) + .map_err(|_| anyhow!("Could not find Cargo.toml for program"))?; + + let target_dir_arg = { + let mut target_dir_arg = OsString::from("--target-dir="); + target_dir_arg.push(expansions_path.join("expand-target")); + target_dir_arg + }; + + let package_name = &cargo + .package + .as_ref() + .ok_or_else(|| anyhow!("Cargo config is missing a package"))? + .name; + let program_expansions_path = expansions_path.join(package_name); + fs::create_dir_all(&program_expansions_path)?; + + let exit = std::process::Command::new("cargo") + .arg("expand") + .arg(target_dir_arg) + .arg(&format!("--package={}", package_name)) + .args(cargo_args) + .stderr(Stdio::inherit()) + .output() + .map_err(|e| anyhow::format_err!("{}", e.to_string()))?; + if !exit.status.success() { + eprintln!("'anchor expand' failed. Perhaps you have not installed 'cargo-expand'? https://github.com/dtolnay/cargo-expand#installation"); + std::process::exit(exit.status.code().unwrap_or(1)); + } + + let version = cargo.version(); + let time = chrono::Utc::now().to_string().replace(' ', "_"); + let file_path = + program_expansions_path.join(format!("{}-{}-{}.rs", package_name, version, time)); + fs::write(&file_path, &exit.stdout).map_err(|e| anyhow::format_err!("{}", e.to_string()))?; + + println!( + "Expanded {} into file {}\n", + package_name, + file_path.to_string_lossy() + ); + Ok(()) +} + #[allow(clippy::too_many_arguments)] pub fn build( cfg_override: &ConfigOverride, diff --git a/docs/src/cli/commands.md b/docs/src/cli/commands.md index 5b6a2d0c..ee5a18ee 100644 --- a/docs/src/cli/commands.md +++ b/docs/src/cli/commands.md @@ -18,6 +18,7 @@ SUBCOMMANDS: build Builds the workspace cluster Cluster commands deploy Deploys each program in the workspace + expand Expands the macros of a program or the workspace help Prints this message or the help of the given subcommand(s) idl Commands for interacting with interface definitions init Initializes a workspace @@ -30,21 +31,6 @@ SUBCOMMANDS: Cargo.toml ``` -## Init - -``` -anchor init -``` - -Initializes a project workspace with the following structure. - -* `Anchor.toml`: Anchor configuration file. -* `Cargo.toml`: Rust workspace configuration file. -* `package.json`: JavaScript dependencies file. -* `programs/`: Directory for Solana program crates. -* `app/`: Directory for your application frontend. -* `tests/`: Directory for JavaScript integration tests. -* `migrations/deploy.js`: Deploy script. ## Build @@ -60,6 +46,25 @@ anchor build --verifiable Runs the build inside a docker image so that the output binary is deterministic (assuming a Cargo.lock file is used). This command must be run from within a single crate subdirectory within the workspace. For example, `programs//`. +## Cluster + +### Cluster list + +``` +anchor cluster list +``` + +This lists cluster endpoints: + +``` +Cluster Endpoints: + +* Mainnet - https://solana-api.projectserum.com +* Mainnet - https://api.mainnet-beta.solana.com +* Devnet - https://api.devnet.solana.com +* Testnet - https://api.testnet.solana.com +``` + ## Deploy ``` @@ -73,64 +78,17 @@ This is different from the `solana program deploy` command, because everytime it it will generate a *new* program address. ::: -## Upgrade +## Expand ``` -anchor upgrade --program-id +anchor expand ``` -Uses Solana's upgradeable BPF loader to upgrade the on chain program code. +If run inside a program folder, expands the macros of the program. -## Test +If run in the workspace but outside a program folder, expands the macros of the workspace. -``` -anchor test -``` - -Run an integration test suite against the configured cluster, deploying new versions -of all workspace programs before running them. - -If the configured network is a localnet, then automatically starts the localnetwork and runs -the test. - -::: tip Note -Be sure to shutdown any other local validators, otherwise `anchor test` will fail to run. - -If you'd prefer to run the program against your local validator use `anchor test --skip-local-validator`. -::: - -When running tests we stream program logs to `.anchor/program-logs/
..log` - -::: tip Note -The Anchor workflow [recommends](https://www.parity.io/paritys-checklist-for-secure-smart-contract-development/) -to test your program using integration tests in a language other -than Rust to make sure that bugs related to syntax misunderstandings -are coverable with tests and not just replicated in tests. -::: - -## Migrate - -``` -anchor migrate -``` - -Runs the deploy script located at `migrations/deploy.js`, injecting a provider configured -from the workspace's `Anchor.toml`. For example, - -```javascript -// File: migrations/deploys.js - -const anchor = require("@project-serum/anchor"); - -module.exports = async function (provider) { - anchor.setProvider(provider); - - // Add your deploy script here. -} -``` - -Migrations are a new feature -and only support this simple deploy script at the moment. +If run with the `--program-name` option, expand only the given program. ## Idl @@ -195,6 +153,46 @@ anchor idl set-authority -n -p Sets a new authority on the IDL account. Both the `new-authority` and `program-id` must be encoded in base 58. +## Init + +``` +anchor init +``` + +Initializes a project workspace with the following structure. + +* `Anchor.toml`: Anchor configuration file. +* `Cargo.toml`: Rust workspace configuration file. +* `package.json`: JavaScript dependencies file. +* `programs/`: Directory for Solana program crates. +* `app/`: Directory for your application frontend. +* `tests/`: Directory for JavaScript integration tests. +* `migrations/deploy.js`: Deploy script. + +## Migrate + +``` +anchor migrate +``` + +Runs the deploy script located at `migrations/deploy.js`, injecting a provider configured +from the workspace's `Anchor.toml`. For example, + +```javascript +// File: migrations/deploys.js + +const anchor = require("@project-serum/anchor"); + +module.exports = async function (provider) { + anchor.setProvider(provider); + + // Add your deploy script here. +} +``` + +Migrations are a new feature +and only support this simple deploy script at the moment. + ## New ``` @@ -203,25 +201,41 @@ anchor new Creates a new program in the workspace's `programs/` directory initialized with boilerplate. -## Cluster - -### Cluster list +## Test ``` -anchor cluster list +anchor test ``` -This lists cluster endpoints: +Run an integration test suit against the configured cluster, deploying new versions +of all workspace programs before running them. + +If the configured network is a localnet, then automatically starts the localnetwork and runs +the test. + +::: tip Note +Be sure to shutdown any other local validators, otherwise `anchor test` will fail to run. + +If you'd prefer to run the program against your local validator use `anchor test --skip-local-validator`. +::: + +When running tests we stream program logs to `.anchor/program-logs/
..log` + +::: tip Note +The Anchor workflow [recommends](https://www.parity.io/paritys-checklist-for-secure-smart-contract-development/) +to test your program using integration tests in a language other +than Rust to make sure that bugs related to syntax misunderstandings +are coverable with tests and not just replicated in tests. +::: + +## Upgrade ``` -Cluster Endpoints: - -* Mainnet - https://solana-api.projectserum.com -* Mainnet - https://api.mainnet-beta.solana.com -* Devnet - https://api.devnet.solana.com -* Testnet - https://api.testnet.solana.com +anchor upgrade --program-id ``` +Uses Solana's upgradeable BPF loader to upgrade the on chain program code. + ## Verify ```