diff --git a/CHANGELOG.md b/CHANGELOG.md index 5861c9802..1f112381b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ incremented for features. ## Features * ts: Address metadata is now optional for `anchor.workspace` clients ([#310](https://github.com/project-serum/anchor/pull/310)). +* cli: Add global options for override Anchor.toml values ([#313](https://github.com/project-serum/anchor/pull/313)). ## [0.6.0] - 2021-05-23 diff --git a/cli/src/config.rs b/cli/src/config.rs index 335d76dfa..144a798d9 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -1,3 +1,4 @@ +use crate::ConfigOverride; use anchor_client::Cluster; use anchor_syn::idl::Idl; use anyhow::{anyhow, Error, Result}; @@ -27,8 +28,25 @@ pub struct ProviderConfig { pub type ClustersConfig = BTreeMap>; impl Config { + pub fn discover( + cfg_override: &ConfigOverride, + ) -> Result)>> { + Config::_discover().map(|opt| { + opt.map(|(mut cfg, cfg_path, cargo_toml)| { + if let Some(cluster) = cfg_override.cluster.clone() { + cfg.provider.cluster = cluster; + } + + if let Some(wallet) = cfg_override.wallet.clone() { + cfg.provider.wallet = wallet; + } + (cfg, cfg_path, cargo_toml) + }) + }) + } + // Searches all parent directories for an Anchor.toml file. - pub fn discover() -> Result)>> { + fn _discover() -> Result)>> { // Set to true if we ever see a Cargo.toml file when traversing the // parent directories. let mut cargo_toml = None; diff --git a/cli/src/main.rs b/cli/src/main.rs index ef767bf03..ced8a7b58 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,6 +1,6 @@ //! CLI for workspace management of anchor programs. -use crate::config::{read_all_programs, Config, Program, ProgramWorkspace}; +use crate::config::{read_all_programs, Config, Program, ProgramWorkspace, WalletPath}; use anchor_client::Cluster; use anchor_lang::idl::{IdlAccount, IdlInstruction}; use anchor_lang::{AccountDeserialize, AnchorDeserialize, AnchorSerialize}; @@ -28,7 +28,6 @@ use std::fs::{self, File}; use std::io::prelude::*; use std::path::{Path, PathBuf}; use std::process::{Child, Stdio}; -use std::str::FromStr; use std::string::ToString; mod config; @@ -41,10 +40,22 @@ const DOCKER_BUILDER_VERSION: &str = VERSION; #[derive(Debug, Clap)] #[clap(version = VERSION)] pub struct Opts { + #[clap(flatten)] + pub cfg_override: ConfigOverride, #[clap(subcommand)] pub command: Command, } +#[derive(Debug, Clap)] +pub struct ConfigOverride { + /// Cluster override. + #[clap(global = true, long = "provider.cluster")] + cluster: Option, + /// Wallet override. + #[clap(global = true, long = "provider.wallet")] + wallet: Option, +} + #[derive(Debug, Clap)] pub enum Command { /// Initializes a workspace. @@ -97,24 +108,13 @@ pub enum Command { }, /// Deploys each program in the workspace. Deploy { - #[clap(short, long)] - url: Option, - #[clap(short, long)] - keypair: Option, #[clap(short, long)] program_name: Option, }, /// Runs the deploy migration script. - Migrate { - #[clap(short, long)] - url: Option, - }, + Migrate, /// Deploys, initializes an IDL, and migrates all in one command. Launch { - #[clap(short, long)] - url: Option, - #[clap(short, long)] - keypair: Option, /// True if the build should be verifiable. If deploying to mainnet, /// this should almost always be set. #[clap(short, long)] @@ -144,14 +144,7 @@ pub enum Command { }, /// Starts a node shell with an Anchor client setup according to the local /// config. - Shell { - /// The cluster config to use. - #[clap(short, long)] - cluster: Option, - /// Local path to the wallet keypair file. - #[clap(short, long)] - wallet: Option, - }, + Shell, } #[derive(Debug, Clap)] @@ -235,43 +228,44 @@ pub enum ClusterCommand { fn main() -> Result<()> { let opts = Opts::parse(); match opts.command { - Command::Init { name, typescript } => init(name, typescript), - Command::New { name } => new(name), - Command::Build { idl, verifiable } => build(idl, verifiable), - Command::Verify { program_id } => verify(program_id), - Command::Deploy { - url, - keypair, - program_name, - } => deploy(url, keypair, program_name), + Command::Init { name, typescript } => init(&opts.cfg_override, name, typescript), + Command::New { name } => new(&opts.cfg_override, name), + Command::Build { idl, verifiable } => build(&opts.cfg_override, idl, verifiable), + Command::Verify { program_id } => verify(&opts.cfg_override, program_id), + Command::Deploy { program_name } => deploy(&opts.cfg_override, program_name), Command::Upgrade { program_id, program_filepath, - } => upgrade(program_id, program_filepath), - Command::Idl { subcmd } => idl(subcmd), - Command::Migrate { url } => migrate(url), + } => upgrade(&opts.cfg_override, program_id, program_filepath), + Command::Idl { subcmd } => idl(&opts.cfg_override, subcmd), + Command::Migrate => migrate(&opts.cfg_override), Command::Launch { - url, - keypair, verifiable, program_name, - } => launch(url, keypair, verifiable, program_name), + } => launch(&opts.cfg_override, verifiable, program_name), Command::Test { skip_deploy, skip_local_validator, skip_build, yarn, file, - } => test(skip_deploy, skip_local_validator, skip_build, yarn, file), + } => test( + &opts.cfg_override, + skip_deploy, + skip_local_validator, + skip_build, + yarn, + file, + ), #[cfg(feature = "dev")] - Command::Airdrop { url } => airdrop(url), + Command::Airdrop => airdrop(cfg_override), Command::Cluster { subcmd } => cluster(subcmd), - Command::Shell { cluster, wallet } => shell(cluster, wallet), + Command::Shell => shell(&opts.cfg_override), } } -fn init(name: String, typescript: bool) -> Result<()> { - let cfg = Config::discover()?; +fn init(cfg_override: &ConfigOverride, name: String, typescript: bool) -> Result<()> { + let cfg = Config::discover(cfg_override)?; if cfg.is_some() { println!("Anchor workspace already initialized"); @@ -328,8 +322,8 @@ fn init(name: String, typescript: bool) -> Result<()> { } // Creates a new program crate in the `programs/` directory. -fn new(name: String) -> Result<()> { - with_workspace(|_cfg, path, _cargo| { +fn new(cfg_override: &ConfigOverride, name: String) -> Result<()> { + with_workspace(cfg_override, |_cfg, path, _cargo| { match path.parent() { None => { println!("Unable to make new program"); @@ -357,8 +351,8 @@ fn new_program(name: &str) -> Result<()> { Ok(()) } -fn build(idl: Option, verifiable: bool) -> Result<()> { - let (cfg, path, cargo) = Config::discover()?.expect("Not in workspace."); +fn build(cfg_override: &ConfigOverride, idl: Option, verifiable: bool) -> Result<()> { + let (cfg, path, cargo) = Config::discover(cfg_override)?.expect("Not in workspace."); let idl_out = match idl { Some(idl) => Some(PathBuf::from(idl)), None => { @@ -524,14 +518,14 @@ fn _build_cwd(idl_out: Option) -> Result<()> { write_idl(&idl, OutFile::File(out)) } -fn verify(program_id: Pubkey) -> Result<()> { - let (cfg, _path, cargo) = Config::discover()?.expect("Not in workspace."); +fn verify(cfg_override: &ConfigOverride, program_id: Pubkey) -> Result<()> { + let (cfg, _path, cargo) = Config::discover(cfg_override)?.expect("Not in workspace."); let cargo = cargo.ok_or(anyhow!("Must be inside program subdirectory."))?; let program_dir = cargo.parent().unwrap(); // Build the program we want to verify. let cur_dir = std::env::current_dir()?; - build(None, true)?; + build(cfg_override, None, true)?; std::env::set_current_dir(&cur_dir)?; let local_idl = extract_idl("src/lib.rs")?; @@ -545,7 +539,7 @@ fn verify(program_id: Pubkey) -> Result<()> { // Verify IDL (only if it's not a buffer account). if !is_buffer { std::env::set_current_dir(program_dir)?; - let deployed_idl = fetch_idl(program_id)?; + let deployed_idl = fetch_idl(cfg_override, program_id)?; if local_idl != deployed_idl { println!("Error: IDLs don't match"); std::process::exit(1); @@ -608,8 +602,10 @@ fn verify_bin(program_id: Pubkey, bin_path: &Path, cluster: &str) -> Result Result { - let cfg = Config::discover()?.expect("Inside a workspace").0; +fn fetch_idl(cfg_override: &ConfigOverride, idl_addr: Pubkey) -> Result { + let cfg = Config::discover(cfg_override)? + .expect("Inside a workspace") + .0; let client = RpcClient::new(cfg.provider.cluster.url().to_string()); let mut account = client @@ -640,35 +636,37 @@ fn extract_idl(file: &str) -> Result { anchor_syn::parser::file::parse(&*file) } -fn idl(subcmd: IdlCommand) -> Result<()> { +fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> { match subcmd { IdlCommand::Init { program_id, filepath, - } => idl_init(program_id, filepath), + } => idl_init(cfg_override, program_id, filepath), IdlCommand::WriteBuffer { program_id, filepath, - } => idl_write_buffer(program_id, filepath).map(|_| ()), - IdlCommand::SetBuffer { program_id, buffer } => idl_set_buffer(program_id, buffer), + } => idl_write_buffer(cfg_override, program_id, filepath).map(|_| ()), + IdlCommand::SetBuffer { program_id, buffer } => { + idl_set_buffer(cfg_override, program_id, buffer) + } IdlCommand::Upgrade { program_id, filepath, - } => idl_upgrade(program_id, filepath), + } => idl_upgrade(cfg_override, program_id, filepath), IdlCommand::SetAuthority { program_id, address, new_authority, - } => idl_set_authority(program_id, address, new_authority), - IdlCommand::EraseAuthority { program_id } => idl_erase_authority(program_id), - IdlCommand::Authority { program_id } => idl_authority(program_id), + } => idl_set_authority(cfg_override, program_id, address, new_authority), + IdlCommand::EraseAuthority { program_id } => idl_erase_authority(cfg_override, program_id), + IdlCommand::Authority { program_id } => idl_authority(cfg_override, program_id), IdlCommand::Parse { file, out } => idl_parse(file, out), - IdlCommand::Fetch { address, out } => idl_fetch(address, out), + IdlCommand::Fetch { address, out } => idl_fetch(cfg_override, address, out), } } -fn idl_init(program_id: Pubkey, idl_filepath: String) -> Result<()> { - with_workspace(|cfg, _path, _cargo| { +fn idl_init(cfg_override: &ConfigOverride, program_id: Pubkey, idl_filepath: String) -> Result<()> { + with_workspace(cfg_override, |cfg, _path, _cargo| { let keypair = cfg.provider.wallet.to_string(); let bytes = std::fs::read(idl_filepath)?; @@ -681,8 +679,12 @@ fn idl_init(program_id: Pubkey, idl_filepath: String) -> Result<()> { }) } -fn idl_write_buffer(program_id: Pubkey, idl_filepath: String) -> Result { - with_workspace(|cfg, _path, _cargo| { +fn idl_write_buffer( + cfg_override: &ConfigOverride, + program_id: Pubkey, + idl_filepath: String, +) -> Result { + with_workspace(cfg_override, |cfg, _path, _cargo| { let keypair = cfg.provider.wallet.to_string(); let bytes = std::fs::read(idl_filepath)?; @@ -697,8 +699,8 @@ fn idl_write_buffer(program_id: Pubkey, idl_filepath: String) -> Result }) } -fn idl_set_buffer(program_id: Pubkey, buffer: Pubkey) -> Result<()> { - with_workspace(|cfg, _path, _cargo| { +fn idl_set_buffer(cfg_override: &ConfigOverride, program_id: Pubkey, buffer: Pubkey) -> Result<()> { + with_workspace(cfg_override, |cfg, _path, _cargo| { let keypair = solana_sdk::signature::read_keypair_file(&cfg.provider.wallet.to_string()) .map_err(|_| anyhow!("Unable to read keypair file"))?; let client = RpcClient::new(cfg.provider.cluster.url().to_string()); @@ -742,13 +744,17 @@ fn idl_set_buffer(program_id: Pubkey, buffer: Pubkey) -> Result<()> { }) } -fn idl_upgrade(program_id: Pubkey, idl_filepath: String) -> Result<()> { - let buffer = idl_write_buffer(program_id, idl_filepath)?; - idl_set_buffer(program_id, buffer) +fn idl_upgrade( + cfg_override: &ConfigOverride, + program_id: Pubkey, + idl_filepath: String, +) -> Result<()> { + let buffer = idl_write_buffer(cfg_override, program_id, idl_filepath)?; + idl_set_buffer(cfg_override, program_id, buffer) } -fn idl_authority(program_id: Pubkey) -> Result<()> { - with_workspace(|cfg, _path, _cargo| { +fn idl_authority(cfg_override: &ConfigOverride, program_id: Pubkey) -> Result<()> { + with_workspace(cfg_override, |cfg, _path, _cargo| { let client = RpcClient::new(cfg.provider.cluster.url().to_string()); let idl_address = { let account = client @@ -773,11 +779,12 @@ fn idl_authority(program_id: Pubkey) -> Result<()> { } fn idl_set_authority( + cfg_override: &ConfigOverride, program_id: Pubkey, address: Option, new_authority: Pubkey, ) -> Result<()> { - with_workspace(|cfg, _path, _cargo| { + with_workspace(cfg_override, |cfg, _path, _cargo| { // Misc. let idl_address = match address { None => IdlAccount::address(&program_id), @@ -826,7 +833,7 @@ fn idl_set_authority( }) } -fn idl_erase_authority(program_id: Pubkey) -> Result<()> { +fn idl_erase_authority(cfg_override: &ConfigOverride, program_id: Pubkey) -> Result<()> { println!("Are you sure you want to erase the IDL authority: [y/n]"); let stdin = std::io::stdin(); @@ -839,7 +846,7 @@ fn idl_erase_authority(program_id: Pubkey) -> Result<()> { // Program will treat the zero authority as erased. let new_authority = Pubkey::new_from_array([0u8; 32]); - idl_set_authority(program_id, None, new_authority)?; + idl_set_authority(cfg_override, program_id, None, new_authority)?; Ok(()) } @@ -917,8 +924,8 @@ fn idl_parse(file: String, out: Option) -> Result<()> { write_idl(&idl, out) } -fn idl_fetch(address: Pubkey, out: Option) -> Result<()> { - let idl = fetch_idl(address)?; +fn idl_fetch(cfg_override: &ConfigOverride, address: Pubkey, out: Option) -> Result<()> { + let idl = fetch_idl(cfg_override, address)?; let out = match out { None => OutFile::Stdout, Some(out) => OutFile::File(PathBuf::from(out)), @@ -942,18 +949,19 @@ enum OutFile { // Builds, deploys, and tests all workspace programs in a single command. fn test( + cfg_override: &ConfigOverride, skip_deploy: bool, skip_local_validator: bool, skip_build: bool, use_yarn: bool, file: Option, ) -> Result<()> { - with_workspace(|cfg, _path, _cargo| { + with_workspace(cfg_override, |cfg, _path, _cargo| { // Bootup validator, if needed. let validator_handle = match cfg.provider.cluster.url() { "http://127.0.0.1:8899" => { if !skip_build { - build(None, false)?; + build(cfg_override, None, false)?; } let flags = match skip_deploy { true => None, @@ -966,8 +974,8 @@ fn test( } _ => { if !skip_deploy { - build(None, false)?; - deploy(None, None, None)?; + build(cfg_override, None, false)?; + deploy(cfg_override, None)?; } None } @@ -1180,23 +1188,17 @@ fn start_test_validator(cfg: &Config, flags: Option>) -> Result, - keypair: Option, - program_name: Option, -) -> Result<()> { - _deploy(url, keypair, program_name).map(|_| ()) +fn deploy(cfg_override: &ConfigOverride, program_name: Option) -> Result<()> { + _deploy(cfg_override, program_name).map(|_| ()) } fn _deploy( - url: Option, - keypair: Option, + cfg_override: &ConfigOverride, program_str: Option, ) -> Result> { - with_workspace(|cfg, _path, _cargo| { - // Fallback to config vars if not provided via CLI. - let url = url.unwrap_or_else(|| cfg.provider.cluster.url().to_string()); - let keypair = keypair.unwrap_or_else(|| cfg.provider.wallet.to_string()); + with_workspace(cfg_override, |cfg, _path, _cargo| { + let url = cfg.provider.cluster.url().to_string(); + let keypair = cfg.provider.wallet.to_string(); // Deploy the programs. println!("Deploying workspace: {}", url); @@ -1265,11 +1267,15 @@ fn _deploy( }) } -fn upgrade(program_id: Pubkey, program_filepath: String) -> Result<()> { +fn upgrade( + cfg_override: &ConfigOverride, + program_id: Pubkey, + program_filepath: String, +) -> Result<()> { let path: PathBuf = program_filepath.parse().unwrap(); let program_filepath = path.canonicalize()?.display().to_string(); - with_workspace(|cfg, _path, _cargo| { + with_workspace(cfg_override, |cfg, _path, _cargo| { let exit = std::process::Command::new("solana") .arg("program") .arg("deploy") @@ -1293,18 +1299,16 @@ fn upgrade(program_id: Pubkey, program_filepath: String) -> Result<()> { } fn launch( - url: Option, - keypair: Option, + cfg_override: &ConfigOverride, verifiable: bool, program_name: Option, ) -> Result<()> { // Build and deploy. - build(None, verifiable)?; - let programs = _deploy(url.clone(), keypair.clone(), program_name.clone())?; + build(cfg_override, None, verifiable)?; + let programs = _deploy(cfg_override, program_name.clone())?; - with_workspace(|cfg, _path, _cargo| { - let url = url.unwrap_or_else(|| cfg.provider.cluster.url().to_string()); - let keypair = keypair.unwrap_or_else(|| cfg.provider.wallet.to_string()); + with_workspace(cfg_override, |cfg, _path, _cargo| { + let keypair = cfg.provider.wallet.to_string(); // Add metadata to all IDLs. for (address, program) in programs { @@ -1316,7 +1320,7 @@ fn launch( // Run migration script. if Path::new("migrations/deploy.js").exists() || Path::new("migrations/deploy.ts").exists() { - migrate(Some(url))?; + migrate(cfg_override)?; } Ok(()) @@ -1467,11 +1471,11 @@ fn serialize_idl_ix(ix_inner: anchor_lang::idl::IdlInstruction) -> Result) -> Result<()> { - with_workspace(|cfg, _path, _cargo| { +fn migrate(cfg_override: &ConfigOverride) -> Result<()> { + with_workspace(cfg_override, |cfg, _path, _cargo| { println!("Running migration deploy script"); - let url = url.unwrap_or_else(|| cfg.provider.cluster.url().to_string()); + let url = cfg.provider.cluster.url().to_string(); let cur_dir = std::env::current_dir()?; let module_path = cur_dir.join("migrations/deploy.js"); @@ -1521,7 +1525,10 @@ fn migrate(url: Option) -> Result<()> { } fn set_workspace_dir_or_exit() { - let d = match Config::discover() { + let d = match Config::discover(&ConfigOverride { + cluster: None, + wallet: None, + }) { Err(_) => { println!("Not in anchor workspace."); std::process::exit(1); @@ -1550,8 +1557,10 @@ fn set_workspace_dir_or_exit() { } #[cfg(feature = "dev")] -fn airdrop(url: Option) -> Result<()> { - let url = url.unwrap_or_else(|| "https://devnet.solana.com".to_string()); +fn airdrop(cfg_override: &ConfigOverride) -> Result<()> { + let url = cfg_override + .cluster + .unwrap_or_else(|| "https://devnet.solana.com".to_string()); loop { let exit = std::process::Command::new("solana") .arg("airdrop") @@ -1579,22 +1588,14 @@ fn cluster(_cmd: ClusterCommand) -> Result<()> { Ok(()) } -fn shell(cluster: Option, wallet: Option) -> Result<()> { - with_workspace(|cfg, _path, _cargo| { - let cluster = match cluster { - None => cfg.provider.cluster.clone(), - Some(c) => Cluster::from_str(&c)?, - }; - let wallet = match wallet { - None => cfg.provider.wallet.to_string(), - Some(c) => c, - }; +fn shell(cfg_override: &ConfigOverride) -> Result<()> { + with_workspace(cfg_override, |cfg, _path, _cargo| { let programs = { let idls: HashMap = read_all_programs()? .iter() .map(|program| (program.idl.name.clone(), program.idl.clone())) .collect(); - match cfg.clusters.get(&cluster) { + match cfg.clusters.get(&cfg.provider.cluster) { None => Vec::new(), Some(programs) => programs .iter() @@ -1612,7 +1613,11 @@ fn shell(cluster: Option, wallet: Option) -> Result<()> { .collect::>(), } }; - let js_code = template::node_shell(cluster.url(), &wallet, programs)?; + let js_code = template::node_shell( + cfg.provider.cluster.url(), + &cfg.provider.wallet.to_string(), + programs, + )?; let mut child = std::process::Command::new("node") .args(&["-e", &js_code, "-i", "--experimental-repl-await"]) .stdout(Stdio::inherit()) @@ -1634,14 +1639,18 @@ fn shell(cluster: Option, wallet: Option) -> Result<()> { // // The closure passed into this function must never change the working directory // to be outside the workspace. Doing so will have undefined behavior. -fn with_workspace(f: impl FnOnce(&Config, PathBuf, Option) -> R) -> R { +fn with_workspace( + cfg_override: &ConfigOverride, + f: impl FnOnce(&Config, PathBuf, Option) -> R, +) -> R { set_workspace_dir_or_exit(); clear_program_keys().unwrap(); - let (cfg, cfg_path, cargo_toml) = Config::discover() + let (cfg, cfg_path, cargo_toml) = Config::discover(cfg_override) .expect("Previously set the workspace dir") .expect("Anchor.toml must always exist"); + let r = f(&cfg, cfg_path, cargo_toml); set_workspace_dir_or_exit();