diff --git a/CHANGELOG.md b/CHANGELOG.md index ab6895f370..d8e6286125 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ incremented for features. ## [Unreleased] +### Features + +* cli: Add keys `members` / `exclude` in config `programs` section ([#546](https://github.com/project-serum/anchor/pull/546)). + ### Breaking Changes * lang: `CpiAccount::reload` mutates the existing struct instead of returning a new one ([#526](https://github.com/project-serum/anchor/pull/526)). diff --git a/cli/src/config.rs b/cli/src/config.rs index 5dbcab9339..538bf0f4e2 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -19,6 +19,7 @@ pub struct Config { pub clusters: ClustersConfig, pub scripts: ScriptsConfig, pub test: Option, + pub workspace: WorkspaceConfig, } #[derive(Debug, Default)] @@ -31,6 +32,14 @@ pub type ScriptsConfig = BTreeMap; pub type ClustersConfig = BTreeMap>; +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct WorkspaceConfig { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub members: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub exclude: Vec, +} + impl Config { pub fn discover( cfg_override: &ConfigOverride, @@ -96,6 +105,51 @@ impl Config { solana_sdk::signature::read_keypair_file(&self.provider.wallet.to_string()) .map_err(|_| anyhow!("Unable to read keypair file")) } + + pub fn get_program_list(&self, path: PathBuf) -> Result> { + let mut programs = vec![]; + for f in fs::read_dir(path)? { + let path = f?.path(); + let program = path + .components() + .last() + .map(|c| c.as_os_str().to_string_lossy().into_owned()) + .expect("failed to get program from path"); + + match ( + self.workspace.members.is_empty(), + self.workspace.exclude.is_empty(), + ) { + (true, true) => programs.push(path), + (true, false) => { + if !self.workspace.exclude.contains(&program) { + programs.push(path); + } + } + (false, _) => { + if self.workspace.members.contains(&program) { + programs.push(path); + } + } + } + } + Ok(programs) + } + + // TODO: this should read idl dir instead of parsing source. + pub fn read_all_programs(&self) -> Result> { + let mut r = vec![]; + for path in self.get_program_list("programs".into())? { + let idl = anchor_syn::idl::file::parse(path.join("src/lib.rs"))?; + let lib_name = extract_lib_name(&path.join("Cargo.toml"))?; + r.push(Program { + lib_name, + path, + idl, + }); + } + Ok(r) + } } // Pubkey serializes as a byte array so use this type a hack to serialize @@ -106,6 +160,7 @@ struct _Config { test: Option, scripts: Option, clusters: Option>>, + workspace: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -135,6 +190,7 @@ impl ToString for Config { false => Some(self.scripts.clone()), }, clusters, + workspace: Some(self.workspace.clone()), }; toml::to_string(&cfg).expect("Must be well formed") @@ -155,6 +211,19 @@ impl FromStr for Config { scripts: cfg.scripts.unwrap_or_else(BTreeMap::new), test: cfg.test, clusters: cfg.clusters.map_or(Ok(BTreeMap::new()), deser_clusters)?, + workspace: cfg.workspace.map(|workspace| { + let (members, exclude) = match (workspace.members.is_empty(), workspace.exclude.is_empty()) { + (true, true) => (vec![], vec![]), + (true, false) => (vec![], workspace.exclude), + (false, is_empty) => { + if !is_empty { + println!("Fields `members` and `exclude` in `[workspace]` section are not compatible, only `members` will be used."); + } + (workspace.members, vec![]) + } + }; + WorkspaceConfig { members, exclude } + }).unwrap_or_default() }) } } @@ -224,23 +293,6 @@ pub struct GenesisEntry { pub program: String, } -// TODO: this should read idl dir instead of parsing source. -pub fn read_all_programs() -> Result> { - let files = fs::read_dir("programs")?; - let mut r = vec![]; - for f in files { - let path = f?.path(); - let idl = anchor_syn::idl::file::parse(path.join("src/lib.rs"))?; - let lib_name = extract_lib_name(&path.join("Cargo.toml"))?; - r.push(Program { - lib_name, - path, - idl, - }); - } - Ok(r) -} - pub fn extract_lib_name(path: impl AsRef) -> Result { let mut toml = File::open(path)?; let mut contents = String::new(); diff --git a/cli/src/main.rs b/cli/src/main.rs index 5ffafd8eab..2f39fadc11 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, WalletPath}; +use crate::config::{Config, Program, ProgramWorkspace, WalletPath}; use anchor_client::Cluster; use anchor_lang::idl::{IdlAccount, IdlInstruction}; use anchor_lang::{AccountDeserialize, AnchorDeserialize, AnchorSerialize}; @@ -364,15 +364,16 @@ fn build( verifiable: bool, program_name: Option, ) -> Result<()> { + let (cfg, path, cargo) = Config::discover(cfg_override)?.expect("Not in workspace."); + if let Some(program_name) = program_name { - for program in read_all_programs()? { + for program in cfg.read_all_programs()? { let p = program.path.file_name().unwrap().to_str().unwrap(); if program_name.as_str() == p { std::env::set_current_dir(&program.path)?; } } } - let (cfg, path, cargo) = Config::discover(cfg_override)?.expect("Not in workspace."); let idl_out = match idl { Some(idl) => Some(PathBuf::from(idl)), None => { @@ -395,7 +396,7 @@ fn build( } fn build_all( - _cfg: &Config, + cfg: &Config, cfg_path: PathBuf, idl_out: Option, verifiable: bool, @@ -404,9 +405,7 @@ fn build_all( let r = match cfg_path.parent() { None => Err(anyhow!("Invalid Anchor.toml at {}", cfg_path.display())), Some(parent) => { - let files = fs::read_dir(parent.join("programs"))?; - for f in files { - let p = f?.path(); + for p in cfg.get_program_list(parent.join("programs"))? { build_cwd( cfg_path.as_path(), p.join("Cargo.toml"), @@ -1002,7 +1001,7 @@ fn test( } // Setup log reader. - let log_streams = stream_logs(cfg.provider.cluster.url()); + let log_streams = stream_logs(cfg); // Run the tests. let test_result: Result<_> = { @@ -1065,7 +1064,7 @@ fn test( // in the genesis block. This allows us to run tests without every deploying. fn genesis_flags(cfg: &Config) -> Result> { let mut flags = Vec::new(); - for mut program in read_all_programs()? { + for mut program in cfg.read_all_programs()? { let binary_path = program.binary_path().display().to_string(); let kp = Keypair::generate(&mut OsRng); @@ -1094,14 +1093,14 @@ fn genesis_flags(cfg: &Config) -> Result> { Ok(flags) } -fn stream_logs(url: &str) -> Result> { +fn stream_logs(config: &Config) -> Result> { let program_logs_dir = ".anchor/program-logs"; if Path::new(program_logs_dir).exists() { std::fs::remove_dir_all(program_logs_dir)?; } fs::create_dir_all(program_logs_dir)?; let mut handles = vec![]; - for program in read_all_programs()? { + for program in config.read_all_programs()? { let mut file = File::open(&format!("target/idl/{}.json", program.lib_name))?; let mut contents = vec![]; file.read_to_end(&mut contents)?; @@ -1120,7 +1119,7 @@ fn stream_logs(url: &str) -> Result> { .arg("logs") .arg(metadata.address) .arg("--url") - .arg(url) + .arg(config.provider.cluster.url()) .stdout(stdio) .spawn()?; handles.push(child); @@ -1197,7 +1196,7 @@ fn _deploy( let mut programs = Vec::new(); - for mut program in read_all_programs()? { + for mut program in cfg.read_all_programs()? { if let Some(single_prog_str) = &program_str { let program_name = program.path.file_name().unwrap().to_str().unwrap(); if single_prog_str.as_str() != program_name { @@ -1320,8 +1319,13 @@ fn launch( // The Solana CLI doesn't redeploy a program if this file exists. // So remove it to make all commands explicit. -fn clear_program_keys() -> Result<()> { - for program in read_all_programs()? { +fn clear_program_keys(cfg_override: &ConfigOverride) -> Result<()> { + let config = Config::discover(cfg_override) + .unwrap_or_default() + .unwrap_or_default() + .0; + + for program in config.read_all_programs()? { let anchor_keypair_path = program.anchor_keypair_path(); if Path::exists(&anchor_keypair_path) { std::fs::remove_file(anchor_keypair_path).expect("Always remove"); @@ -1576,7 +1580,8 @@ fn cluster(_cmd: ClusterCommand) -> Result<()> { fn shell(cfg_override: &ConfigOverride) -> Result<()> { with_workspace(cfg_override, |cfg, _path, _cargo| { let programs = { - let mut idls: HashMap = read_all_programs()? + let mut idls: HashMap = cfg + .read_all_programs()? .iter() .map(|program| (program.idl.name.clone(), program.idl.clone())) .collect(); @@ -1664,7 +1669,7 @@ fn with_workspace( ) -> R { set_workspace_dir_or_exit(); - clear_program_keys().unwrap(); + clear_program_keys(cfg_override).unwrap(); let (cfg, cfg_path, cargo_toml) = Config::discover(cfg_override) .expect("Previously set the workspace dir") @@ -1673,7 +1678,7 @@ fn with_workspace( let r = f(&cfg, cfg_path, cargo_toml); set_workspace_dir_or_exit(); - clear_program_keys().unwrap(); + clear_program_keys(cfg_override).unwrap(); r } diff --git a/examples/misc/Anchor.toml b/examples/misc/Anchor.toml index 8c266da82b..f1fbaa1e5c 100644 --- a/examples/misc/Anchor.toml +++ b/examples/misc/Anchor.toml @@ -5,3 +5,6 @@ wallet = "~/.config/solana/id.json" [[test.genesis]] address = "FtMNMKp9DZHKWUyVAsj3Q5QV8ow4P3fUPP7ZrWEQJzKr" program = "./target/deploy/misc.so" + +[workspace] +exclude = ["shared"] diff --git a/examples/misc/programs/shared/Cargo.toml b/examples/misc/programs/shared/Cargo.toml new file mode 100644 index 0000000000..bb16cb94bc --- /dev/null +++ b/examples/misc/programs/shared/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "shared" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/examples/misc/programs/shared/src/lib.rs b/examples/misc/programs/shared/src/lib.rs new file mode 100644 index 0000000000..31e1bb209f --- /dev/null +++ b/examples/misc/programs/shared/src/lib.rs @@ -0,0 +1,7 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +} diff --git a/examples/typescript/Anchor.toml b/examples/typescript/Anchor.toml index b9705f038d..2c7f889ba7 100644 --- a/examples/typescript/Anchor.toml +++ b/examples/typescript/Anchor.toml @@ -1,3 +1,7 @@ [provider] cluster = "localnet" wallet = "~/.config/solana/id.json" + +[workspace] +members = ["typescript"] +exclude = ["typescript"] diff --git a/examples/typescript/programs/shared/Cargo.toml b/examples/typescript/programs/shared/Cargo.toml new file mode 100644 index 0000000000..bb16cb94bc --- /dev/null +++ b/examples/typescript/programs/shared/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "shared" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/examples/typescript/programs/shared/src/lib.rs b/examples/typescript/programs/shared/src/lib.rs new file mode 100644 index 0000000000..31e1bb209f --- /dev/null +++ b/examples/typescript/programs/shared/src/lib.rs @@ -0,0 +1,7 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +} diff --git a/examples/zero-copy/Anchor.toml b/examples/zero-copy/Anchor.toml index b9705f038d..2f62faf568 100644 --- a/examples/zero-copy/Anchor.toml +++ b/examples/zero-copy/Anchor.toml @@ -1,3 +1,6 @@ [provider] cluster = "localnet" wallet = "~/.config/solana/id.json" + +[workspace] +members = ["zero-copy"] diff --git a/examples/zero-copy/programs/shared/Cargo.toml b/examples/zero-copy/programs/shared/Cargo.toml new file mode 100644 index 0000000000..bb16cb94bc --- /dev/null +++ b/examples/zero-copy/programs/shared/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "shared" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/examples/zero-copy/programs/shared/src/lib.rs b/examples/zero-copy/programs/shared/src/lib.rs new file mode 100644 index 0000000000..31e1bb209f --- /dev/null +++ b/examples/zero-copy/programs/shared/src/lib.rs @@ -0,0 +1,7 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +}