Browse Source

cli: Add keys include/exclude in programs section (#546)

Kirill Fomichev 4 years ago
parent
commit
b8dae74836

+ 4 - 0
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
 
 * ts: Use `hex` by default for decoding Instruction ([#547](https://github.com/project-serum/anchor/pull/547)).

+ 69 - 17
cli/src/config.rs

@@ -19,6 +19,7 @@ pub struct Config {
     pub clusters: ClustersConfig,
     pub scripts: ScriptsConfig,
     pub test: Option<Test>,
+    pub workspace: WorkspaceConfig,
 }
 
 #[derive(Debug, Default)]
@@ -31,6 +32,14 @@ pub type ScriptsConfig = BTreeMap<String, String>;
 
 pub type ClustersConfig = BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>;
 
+#[derive(Debug, Default, Clone, Serialize, Deserialize)]
+pub struct WorkspaceConfig {
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub members: Vec<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub exclude: Vec<String>,
+}
+
 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<Vec<PathBuf>> {
+        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<Vec<Program>> {
+        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<Test>,
     scripts: Option<ScriptsConfig>,
     clusters: Option<BTreeMap<String, BTreeMap<String, serde_json::Value>>>,
+    workspace: Option<WorkspaceConfig>,
 }
 
 #[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<Vec<Program>> {
-    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<Path>) -> Result<String> {
     let mut toml = File::open(path)?;
     let mut contents = String::new();

+ 23 - 18
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<String>,
 ) -> 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<PathBuf>,
     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<Vec<String>> {
     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<Vec<String>> {
     Ok(flags)
 }
 
-fn stream_logs(url: &str) -> Result<Vec<std::process::Child>> {
+fn stream_logs(config: &Config) -> Result<Vec<std::process::Child>> {
     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<Vec<std::process::Child>> {
             .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<String, Idl> = read_all_programs()?
+            let mut idls: HashMap<String, Idl> = cfg
+                .read_all_programs()?
                 .iter()
                 .map(|program| (program.idl.name.clone(), program.idl.clone()))
                 .collect();
@@ -1664,7 +1669,7 @@ fn with_workspace<R>(
 ) -> 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<R>(
     let r = f(&cfg, cfg_path, cargo_toml);
 
     set_workspace_dir_or_exit();
-    clear_program_keys().unwrap();
+    clear_program_keys(cfg_override).unwrap();
 
     r
 }

+ 3 - 0
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"]

+ 8 - 0
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]

+ 7 - 0
examples/misc/programs/shared/src/lib.rs

@@ -0,0 +1,7 @@
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn it_works() {
+        assert_eq!(2 + 2, 4);
+    }
+}

+ 4 - 0
examples/typescript/Anchor.toml

@@ -1,3 +1,7 @@
 [provider]
 cluster = "localnet"
 wallet = "~/.config/solana/id.json"
+
+[workspace]
+members = ["typescript"]
+exclude = ["typescript"]

+ 8 - 0
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]

+ 7 - 0
examples/typescript/programs/shared/src/lib.rs

@@ -0,0 +1,7 @@
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn it_works() {
+        assert_eq!(2 + 2, 4);
+    }
+}

+ 3 - 0
examples/zero-copy/Anchor.toml

@@ -1,3 +1,6 @@
 [provider]
 cluster = "localnet"
 wallet = "~/.config/solana/id.json"
+
+[workspace]
+members = ["zero-copy"]

+ 8 - 0
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]

+ 7 - 0
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);
+    }
+}