Преглед на файлове

cli: Fix Cargo.lock in workspace subdirectories when publishing (#593)

Armani Ferrante преди 4 години
родител
ревизия
bd68e28f15
променени са 6 файла, в които са добавени 280 реда и са изтрити 205 реда
  1. 4 0
      CHANGELOG.md
  2. 1 0
      Cargo.lock
  3. 1 0
      cli/Cargo.toml
  4. 123 65
      cli/src/config.rs
  5. 148 137
      cli/src/lib.rs
  6. 3 3
      docs/src/getting-started/publishing.md

+ 4 - 0
CHANGELOG.md

@@ -15,6 +15,10 @@ incremented for features.
 
 * cli: Programs embedded into genesis during tests will produce program logs.
 
+### Fixes
+
+* cli: Allows Cargo.lock to exist in workspace subdirectories when publishing ([#593](https://github.com/project-serum/anchor/pull/593)).
+
 ## [0.13.0] - 2021-08-08
 
 ### Features

+ 1 - 0
Cargo.lock

@@ -159,6 +159,7 @@ dependencies = [
  "tar",
  "tokio 1.4.0",
  "toml",
+ "walkdir",
 ]
 
 [[package]]

+ 1 - 0
cli/Cargo.toml

@@ -36,3 +36,4 @@ reqwest = { version = "0.11.4", features = ["multipart", "blocking"] }
 tokio = "1.0"
 pathdiff = "0.2.0"
 cargo_toml = "0.9.2"
+walkdir = "2"

+ 123 - 65
cli/src/config.rs

@@ -9,6 +9,7 @@ use std::collections::BTreeMap;
 use std::convert::TryFrom;
 use std::fs::{self, File};
 use std::io::prelude::*;
+use std::ops::Deref;
 use std::path::Path;
 use std::path::PathBuf;
 use std::str::FromStr;
@@ -48,6 +49,68 @@ impl<T> std::convert::AsRef<T> for WithPath<T> {
     }
 }
 
+#[derive(Debug, Clone, PartialEq)]
+pub struct Manifest(cargo_toml::Manifest);
+
+impl Manifest {
+    pub fn from_path(p: impl AsRef<Path>) -> Result<Self> {
+        cargo_toml::Manifest::from_path(p)
+            .map(Manifest)
+            .map_err(Into::into)
+    }
+
+    pub fn lib_name(&self) -> Result<String> {
+        if self.lib.is_some() && self.lib.as_ref().unwrap().name.is_some() {
+            Ok(self
+                .lib
+                .as_ref()
+                .unwrap()
+                .name
+                .as_ref()
+                .unwrap()
+                .to_string())
+        } else {
+            Ok(self
+                .package
+                .as_ref()
+                .ok_or_else(|| anyhow!("package section not provided"))?
+                .name
+                .to_string())
+        }
+    }
+
+    // Climbs each parent directory until we find a Cargo.toml.
+    pub fn discover() -> Result<Option<WithPath<Manifest>>> {
+        let _cwd = std::env::current_dir()?;
+        let mut cwd_opt = Some(_cwd.as_path());
+
+        while let Some(cwd) = cwd_opt {
+            for f in fs::read_dir(cwd)? {
+                let p = f?.path();
+                if let Some(filename) = p.file_name() {
+                    if filename.to_str() == Some("Cargo.toml") {
+                        let m = WithPath::new(Manifest::from_path(&p)?, p);
+                        return Ok(Some(m));
+                    }
+                }
+            }
+
+            // Not found. Go up a directory level.
+            cwd_opt = cwd.parent();
+        }
+
+        Ok(None)
+    }
+}
+
+impl Deref for Manifest {
+    type Target = cargo_toml::Manifest;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
 impl WithPath<Config> {
     pub fn get_program_list(&self) -> Result<Vec<PathBuf>> {
         // Canonicalize the workspace filepaths to compare with relative paths.
@@ -82,7 +145,7 @@ impl WithPath<Config> {
         let mut r = vec![];
         for path in self.get_program_list()? {
             let idl = anchor_syn::idl::file::parse(path.join("src/lib.rs"))?;
-            let lib_name = extract_lib_name(&path.join("Cargo.toml"))?;
+            let lib_name = Manifest::from_path(&path.join("Cargo.toml"))?.lib_name()?;
             r.push(Program {
                 lib_name,
                 path,
@@ -97,16 +160,53 @@ impl WithPath<Config> {
             .workspace
             .members
             .iter()
-            .map(|m| PathBuf::from(m).canonicalize().unwrap())
+            .map(|m| {
+                self.path()
+                    .parent()
+                    .unwrap()
+                    .join(m)
+                    .canonicalize()
+                    .unwrap()
+            })
             .collect();
         let exclude = self
             .workspace
             .exclude
             .iter()
-            .map(|m| PathBuf::from(m).canonicalize().unwrap())
+            .map(|m| {
+                self.path()
+                    .parent()
+                    .unwrap()
+                    .join(m)
+                    .canonicalize()
+                    .unwrap()
+            })
             .collect();
         Ok((members, exclude))
     }
+
+    pub fn get_program(&self, name: &str) -> Result<Option<WithPath<Program>>> {
+        for program in self.read_all_programs()? {
+            let cargo_toml = program.path.join("Cargo.toml");
+            if !cargo_toml.exists() {
+                return Err(anyhow!(
+                    "Did not find Cargo.toml at the path: {}",
+                    program.path.display()
+                ));
+            }
+            let p_lib_name = Manifest::from_path(&cargo_toml)?.lib_name()?;
+            if name == p_lib_name {
+                let path = self
+                    .path()
+                    .parent()
+                    .unwrap()
+                    .canonicalize()?
+                    .join(&program.path);
+                return Ok(Some(WithPath::new(program, path)));
+            }
+        }
+        Ok(None)
+    }
 }
 
 impl<T> std::ops::Deref for WithPath<T> {
@@ -174,71 +274,51 @@ impl Config {
         format!("projectserum/build:v{}", ver)
     }
 
-    pub fn discover(
-        cfg_override: &ConfigOverride,
-    ) -> Result<Option<(WithPath<Config>, Option<PathBuf>)>> {
+    pub fn discover(cfg_override: &ConfigOverride) -> Result<Option<WithPath<Config>>> {
         Config::_discover().map(|opt| {
-            opt.map(|(mut cfg, cargo_toml)| {
+            opt.map(|mut cfg| {
                 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, cargo_toml)
+                cfg
             })
         })
     }
 
-    // Searches all parent directories for an Anchor.toml file.
-    fn _discover() -> Result<Option<(WithPath<Config>, Option<PathBuf>)>> {
-        // Set to true if we ever see a Cargo.toml file when traversing the
-        // parent directories.
-        let mut cargo_toml = None;
-
+    // Climbs each parent directory until we find an Anchor.toml.
+    fn _discover() -> Result<Option<WithPath<Config>>> {
         let _cwd = std::env::current_dir()?;
         let mut cwd_opt = Some(_cwd.as_path());
 
         while let Some(cwd) = cwd_opt {
-            let files = fs::read_dir(cwd)?;
-            // Cargo.toml file for this directory level.
-            let mut cargo_toml_level = None;
-            let mut anchor_toml = None;
-            for f in files {
+            for f in fs::read_dir(cwd)? {
                 let p = f?.path();
                 if let Some(filename) = p.file_name() {
-                    if filename.to_str() == Some("Cargo.toml") {
-                        cargo_toml_level = Some(p);
-                    } else if filename.to_str() == Some("Anchor.toml") {
-                        let mut cfg_file = File::open(&p)?;
-                        let mut cfg_contents = String::new();
-                        cfg_file.read_to_string(&mut cfg_contents)?;
-                        let cfg = cfg_contents.parse()?;
-                        anchor_toml = Some((cfg, p));
+                    if filename.to_str() == Some("Anchor.toml") {
+                        let cfg = Config::from_path(&p)?;
+                        return Ok(Some(WithPath::new(cfg, p)));
                     }
                 }
             }
 
-            // Set the Cargo.toml if it's for a single package, i.e., not the
-            // root workspace Cargo.toml.
-            if cargo_toml.is_none() && cargo_toml_level.is_some() {
-                let toml = cargo_toml::Manifest::from_path(cargo_toml_level.as_ref().unwrap())?;
-                if toml.workspace.is_none() {
-                    cargo_toml = cargo_toml_level;
-                }
-            }
-
-            if let Some((cfg, parent)) = anchor_toml {
-                return Ok(Some((WithPath::new(cfg, parent), cargo_toml)));
-            }
-
             cwd_opt = cwd.parent();
         }
 
         Ok(None)
     }
 
+    fn from_path(p: impl AsRef<Path>) -> Result<Self> {
+        let mut cfg_file = File::open(&p)?;
+        let mut cfg_contents = String::new();
+        cfg_file.read_to_string(&mut cfg_contents)?;
+        let cfg = cfg_contents.parse()?;
+
+        Ok(cfg)
+    }
+
     pub fn wallet_kp(&self) -> Result<Keypair> {
         solana_sdk::signature::read_keypair_file(&self.provider.wallet.to_string())
             .map_err(|_| anyhow!("Unable to read keypair file"))
@@ -382,21 +462,10 @@ pub struct GenesisEntry {
     pub program: String,
 }
 
-pub fn extract_lib_name(cargo_toml: impl AsRef<Path>) -> Result<String> {
-    let cargo_toml = cargo_toml::Manifest::from_path(cargo_toml)?;
-    if cargo_toml.lib.is_some() && cargo_toml.lib.as_ref().unwrap().name.is_some() {
-        Ok(cargo_toml.lib.unwrap().name.unwrap())
-    } else {
-        Ok(cargo_toml
-            .package
-            .ok_or_else(|| anyhow!("Package section not provided"))?
-            .name)
-    }
-}
-
 #[derive(Debug, Clone)]
 pub struct Program {
     pub lib_name: String,
+    // Canonicalized path to the program directory.
     pub path: PathBuf,
     pub idl: Option<Idl>,
 }
@@ -463,7 +532,6 @@ pub struct ProgramWorkspace {
 pub struct AnchorPackage {
     pub name: String,
     pub address: String,
-    pub path: String,
     pub idl: Option<String>,
 }
 
@@ -479,19 +547,9 @@ impl AnchorPackage {
             .ok_or_else(|| anyhow!("Program not provided in Anchor.toml"))?
             .get(&name)
             .ok_or_else(|| anyhow!("Program not provided in Anchor.toml"))?;
-        let path = program_details
-            .path
-            .clone()
-            // TODO: use a default path if one isn't provided?
-            .ok_or_else(|| anyhow!("Path to program binary not provided"))?;
         let idl = program_details.idl.clone();
         let address = program_details.address.to_string();
-        Ok(Self {
-            name,
-            path,
-            address,
-            idl,
-        })
+        Ok(Self { name, address, idl })
     }
 }
 

+ 148 - 137
cli/src/lib.rs

@@ -1,4 +1,6 @@
-use crate::config::{AnchorPackage, Config, ConfigOverride, Program, ProgramWorkspace, WithPath};
+use crate::config::{
+    AnchorPackage, Config, ConfigOverride, Manifest, Program, ProgramWorkspace, WithPath,
+};
 use anchor_client::Cluster;
 use anchor_lang::idl::{IdlAccount, IdlInstruction};
 use anchor_lang::{AccountDeserialize, AnchorDeserialize, AnchorSerialize};
@@ -297,11 +299,8 @@ pub fn entry(opts: Opts) -> Result<()> {
 }
 
 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");
-    }
+    let _ =
+        Config::discover(cfg_override)?.ok_or_else(|| anyhow!("Workspace already initialized"))?;
 
     fs::create_dir(name.clone())?;
     std::env::set_current_dir(&name)?;
@@ -364,7 +363,7 @@ fn init(cfg_override: &ConfigOverride, name: String, typescript: bool) -> Result
 
 // Creates a new program crate in the `programs/<name>` directory.
 fn new(cfg_override: &ConfigOverride, name: String) -> Result<()> {
-    with_workspace(cfg_override, |cfg, _cargo| {
+    with_workspace(cfg_override, |cfg| {
         match cfg.path().parent() {
             None => {
                 println!("Unable to make new program");
@@ -401,41 +400,14 @@ pub fn build(
     stdout: Option<File>, // Used for the package registry server.
     stderr: Option<File>, // Used for the package registry server.
 ) -> Result<()> {
-    // Change directories to the given `program_name`, if given.
-    let (cfg, _cargo) = Config::discover(cfg_override)?.expect("Not in workspace.");
-
-    let mut did_find_program = false;
+    // Change to the workspace member directory, if needed.
     if let Some(program_name) = program_name.as_ref() {
-        for program in cfg.read_all_programs()? {
-            let cargo_toml = program.path.join("Cargo.toml");
-            if !cargo_toml.exists() {
-                return Err(anyhow!(
-                    "Did not find Cargo.toml at the path: {}",
-                    program.path.display()
-                ));
-            }
-            let p_lib_name = config::extract_lib_name(&cargo_toml)?;
-            if program_name.as_str() == p_lib_name {
-                let program_path = cfg
-                    .path()
-                    .parent()
-                    .unwrap()
-                    .canonicalize()?
-                    .join(program.path);
-                std::env::set_current_dir(&program_path)?;
-                did_find_program = true;
-                break;
-            }
-        }
-    }
-    if !did_find_program && program_name.is_some() {
-        return Err(anyhow!(
-            "{} is not part of the workspace",
-            program_name.as_ref().unwrap()
-        ));
+        cd_member(cfg_override, program_name)?;
     }
 
-    let (cfg, cargo) = Config::discover(cfg_override)?.expect("Not in workspace.");
+    let cfg = Config::discover(cfg_override)?.expect("Not in workspace.");
+    let cargo = Manifest::discover()?;
+
     let idl_out = match idl {
         Some(idl) => Some(PathBuf::from(idl)),
         None => {
@@ -454,17 +426,37 @@ pub fn build(
     };
 
     match cargo {
-        None => build_all(&cfg, cfg.path(), idl_out, verifiable, solana_version)?,
-        Some(ct) => build_cwd(
+        // No Cargo.toml so build the entire workspace.
+        None => build_all(
             &cfg,
-            ct,
+            cfg.path(),
             idl_out,
             verifiable,
             solana_version,
             stdout,
             stderr,
         )?,
-    };
+        // If the Cargo.toml is at the root, build the entire workspace.
+        Some(cargo) if cargo.path().parent() == cfg.path().parent() => build_all(
+            &cfg,
+            cfg.path(),
+            idl_out,
+            verifiable,
+            solana_version,
+            stdout,
+            stderr,
+        )?,
+        // Cargo.toml represents a single package. Build it.
+        Some(cargo) => build_cwd(
+            &cfg,
+            cargo.path().to_path_buf(),
+            idl_out,
+            verifiable,
+            solana_version,
+            stdout,
+            stderr,
+        )?,
+    }
 
     set_workspace_dir_or_exit();
 
@@ -477,6 +469,8 @@ fn build_all(
     idl_out: Option<PathBuf>,
     verifiable: bool,
     solana_version: Option<String>,
+    stdout: Option<File>, // Used for the package registry server.
+    stderr: Option<File>, // Used for the package registry server.
 ) -> Result<()> {
     let cur_dir = std::env::current_dir()?;
     let r = match cfg_path.parent() {
@@ -489,8 +483,8 @@ fn build_all(
                     idl_out.clone(),
                     verifiable,
                     solana_version.clone(),
-                    None,
-                    None,
+                    stdout.as_ref().map(|f| f.try_clone()).transpose()?,
+                    stderr.as_ref().map(|f| f.try_clone()).transpose()?,
                 )?;
             }
             Ok(())
@@ -531,7 +525,7 @@ fn build_cwd_verifiable(
 ) -> Result<()> {
     // Create output dirs.
     let workspace_dir = cfg.path().parent().unwrap().canonicalize()?;
-    fs::create_dir_all(workspace_dir.join("target/deploy"))?;
+    fs::create_dir_all(workspace_dir.join("target/verifiable"))?;
     fs::create_dir_all(workspace_dir.join("target/idl"))?;
 
     let container_name = "anchor-program";
@@ -595,7 +589,7 @@ fn docker_build(
     stdout: Option<File>,
     stderr: Option<File>,
 ) -> Result<()> {
-    let binary_name = config::extract_lib_name(&cargo_toml)?;
+    let binary_name = Manifest::from_path(&cargo_toml)?.lib_name()?;
 
     // Docker vars.
     let image_name = cfg.docker();
@@ -718,7 +712,7 @@ fn docker_build(
         .parent()
         .unwrap()
         .canonicalize()?
-        .join(format!("target/deploy/{}.so", binary_name))
+        .join(format!("target/verifiable/{}.so", binary_name))
         .display()
         .to_string();
 
@@ -774,45 +768,14 @@ fn verify(
     program_name: Option<String>,
     solana_version: Option<String>,
 ) -> Result<()> {
-    // Change directories to the given `program_name`, if given.
-    let (cfg, _cargo) = Config::discover(cfg_override)?.expect("Not in workspace.");
-    let mut did_find_program = false;
+    // Change to the workspace member directory, if needed.
     if let Some(program_name) = program_name.as_ref() {
-        for program in cfg.read_all_programs()? {
-            let cargo_toml = program.path.join("Cargo.toml");
-            if !cargo_toml.exists() {
-                return Err(anyhow!(
-                    "Did not find Cargo.toml at the path: {}",
-                    program.path.display()
-                ));
-            }
-            let p_lib_name = config::extract_lib_name(&cargo_toml)?;
-            if program_name.as_str() == p_lib_name {
-                let program_path = cfg
-                    .path()
-                    .parent()
-                    .unwrap()
-                    .canonicalize()?
-                    .join(program.path);
-                std::env::set_current_dir(&program_path)?;
-                did_find_program = true;
-                break;
-            }
-        }
-    }
-    if !did_find_program && program_name.is_some() {
-        return Err(anyhow!(
-            "{} is not part of the workspace",
-            program_name.as_ref().unwrap()
-        ));
+        cd_member(cfg_override, program_name)?;
     }
 
     // Proceed with the command.
-    let (cfg, cargo) = Config::discover(cfg_override)?.expect("Not in workspace.");
-    let cargo = cargo.ok_or_else(|| {
-        anyhow!("Must be inside program subdirectory if no program name is given.")
-    })?;
-    let program_dir = cargo.parent().unwrap();
+    let cfg = Config::discover(cfg_override)?.expect("Not in workspace.");
+    let cargo = Manifest::discover()?.ok_or_else(|| anyhow!("Cargo.toml not found"))?;
 
     // Build the program we want to verify.
     let cur_dir = std::env::current_dir()?;
@@ -831,23 +794,12 @@ fn verify(
     std::env::set_current_dir(&cur_dir)?;
 
     // Verify binary.
-    let binary_name = {
-        let cargo_toml = cargo_toml::Manifest::from_path(&cargo)?;
-        match cargo_toml.lib {
-            None => {
-                cargo_toml
-                    .package
-                    .ok_or_else(|| anyhow!("Package section not provided"))?
-                    .name
-            }
-            Some(lib) => lib.name.ok_or_else(|| anyhow!("Name not provided"))?,
-        }
-    };
+    let binary_name = cargo.lib_name()?;
     let bin_path = cfg
         .path()
         .parent()
         .ok_or_else(|| anyhow!("Unable to find workspace root"))?
-        .join("target/deploy/")
+        .join("target/verifiable/")
         .join(format!("{}.so", binary_name));
 
     let bin_ver = verify_bin(program_id, &bin_path, cfg.provider.cluster.url())?;
@@ -859,7 +811,6 @@ fn verify(
     // Verify IDL (only if it's not a buffer account).
     if let Some(local_idl) = extract_idl("src/lib.rs")? {
         if bin_ver.state != BinVerificationState::Buffer {
-            std::env::set_current_dir(program_dir)?;
             let deployed_idl = fetch_idl(cfg_override, program_id)?;
             if local_idl != deployed_idl {
                 println!("Error: IDLs don't match");
@@ -873,6 +824,27 @@ fn verify(
     Ok(())
 }
 
+fn cd_member(cfg_override: &ConfigOverride, program_name: &str) -> Result<()> {
+    // Change directories to the given `program_name`, if given.
+    let cfg = Config::discover(cfg_override)?.expect("Not in workspace.");
+
+    for program in cfg.read_all_programs()? {
+        let cargo_toml = program.path.join("Cargo.toml");
+        if !cargo_toml.exists() {
+            return Err(anyhow!(
+                "Did not find Cargo.toml at the path: {}",
+                program.path.display()
+            ));
+        }
+        let p_lib_name = Manifest::from_path(&cargo_toml)?.lib_name()?;
+        if program_name == p_lib_name {
+            std::env::set_current_dir(&program.path)?;
+            return Ok(());
+        }
+    }
+    return Err(anyhow!("{} is not part of the workspace", program_name,));
+}
+
 pub fn verify_bin(program_id: Pubkey, bin_path: &Path, cluster: &str) -> Result<BinVerification> {
     let client = RpcClient::new(cluster.to_string());
 
@@ -954,9 +926,7 @@ pub enum BinVerificationState {
 
 // Fetches an IDL for the given program_id.
 fn fetch_idl(cfg_override: &ConfigOverride, idl_addr: Pubkey) -> Result<Idl> {
-    let cfg = Config::discover(cfg_override)?
-        .expect("Inside a workspace")
-        .0;
+    let cfg = Config::discover(cfg_override)?.expect("Inside a workspace");
     let client = RpcClient::new(cfg.provider.cluster.url().to_string());
 
     let mut account = client
@@ -1017,7 +987,7 @@ fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> {
 }
 
 fn idl_init(cfg_override: &ConfigOverride, program_id: Pubkey, idl_filepath: String) -> Result<()> {
-    with_workspace(cfg_override, |cfg, _cargo| {
+    with_workspace(cfg_override, |cfg| {
         let keypair = cfg.provider.wallet.to_string();
 
         let bytes = std::fs::read(idl_filepath)?;
@@ -1035,7 +1005,7 @@ fn idl_write_buffer(
     program_id: Pubkey,
     idl_filepath: String,
 ) -> Result<Pubkey> {
-    with_workspace(cfg_override, |cfg, _cargo| {
+    with_workspace(cfg_override, |cfg| {
         let keypair = cfg.provider.wallet.to_string();
 
         let bytes = std::fs::read(idl_filepath)?;
@@ -1051,7 +1021,7 @@ fn idl_write_buffer(
 }
 
 fn idl_set_buffer(cfg_override: &ConfigOverride, program_id: Pubkey, buffer: Pubkey) -> Result<()> {
-    with_workspace(cfg_override, |cfg, _cargo| {
+    with_workspace(cfg_override, |cfg| {
         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());
@@ -1105,7 +1075,7 @@ fn idl_upgrade(
 }
 
 fn idl_authority(cfg_override: &ConfigOverride, program_id: Pubkey) -> Result<()> {
-    with_workspace(cfg_override, |cfg, _cargo| {
+    with_workspace(cfg_override, |cfg| {
         let client = RpcClient::new(cfg.provider.cluster.url().to_string());
         let idl_address = {
             let account = client
@@ -1135,7 +1105,7 @@ fn idl_set_authority(
     address: Option<Pubkey>,
     new_authority: Pubkey,
 ) -> Result<()> {
-    with_workspace(cfg_override, |cfg, _cargo| {
+    with_workspace(cfg_override, |cfg| {
         // Misc.
         let idl_address = match address {
             None => IdlAccount::address(&program_id),
@@ -1306,7 +1276,7 @@ fn test(
     skip_build: bool,
     extra_args: Vec<String>,
 ) -> Result<()> {
-    with_workspace(cfg_override, |cfg, _cargo| {
+    with_workspace(cfg_override, |cfg| {
         // Build if needed.
         if !skip_build {
             build(cfg_override, None, false, None, None, None, None)?;
@@ -1534,7 +1504,7 @@ fn _deploy(
     cfg_override: &ConfigOverride,
     program_str: Option<String>,
 ) -> Result<Vec<(Pubkey, Program)>> {
-    with_workspace(cfg_override, |cfg, _cargo| {
+    with_workspace(cfg_override, |cfg| {
         let url = cfg.provider.cluster.url().to_string();
         let keypair = cfg.provider.wallet.to_string();
 
@@ -1615,7 +1585,7 @@ fn upgrade(
     let path: PathBuf = program_filepath.parse().unwrap();
     let program_filepath = path.canonicalize()?.display().to_string();
 
-    with_workspace(cfg_override, |cfg, _cargo| {
+    with_workspace(cfg_override, |cfg| {
         let exit = std::process::Command::new("solana")
             .arg("program")
             .arg("deploy")
@@ -1655,7 +1625,7 @@ fn launch(
     )?;
     let programs = _deploy(cfg_override, program_name)?;
 
-    with_workspace(cfg_override, |cfg, _cargo| {
+    with_workspace(cfg_override, |cfg| {
         let keypair = cfg.provider.wallet.to_string();
 
         // Add metadata to all IDLs.
@@ -1680,10 +1650,7 @@ 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(cfg_override: &ConfigOverride) -> Result<()> {
-    let config = Config::discover(cfg_override)
-        .unwrap_or_default()
-        .unwrap()
-        .0;
+    let config = Config::discover(cfg_override).unwrap_or_default().unwrap();
 
     for program in config.read_all_programs()? {
         let anchor_keypair_path = program.anchor_keypair_path();
@@ -1827,7 +1794,7 @@ fn serialize_idl_ix(ix_inner: anchor_lang::idl::IdlInstruction) -> Result<Vec<u8
 }
 
 fn migrate(cfg_override: &ConfigOverride) -> Result<()> {
-    with_workspace(cfg_override, |cfg, _cargo| {
+    with_workspace(cfg_override, |cfg| {
         println!("Running migration deploy script");
 
         let url = cfg.provider.cluster.url().to_string();
@@ -1874,10 +1841,7 @@ fn migrate(cfg_override: &ConfigOverride) -> Result<()> {
 }
 
 fn set_workspace_dir_or_exit() {
-    let d = match Config::discover(&ConfigOverride {
-        cluster: None,
-        wallet: None,
-    }) {
+    let d = match Config::discover(&ConfigOverride::default()) {
         Err(_) => {
             println!("Not in anchor workspace.");
             std::process::exit(1);
@@ -1889,7 +1853,7 @@ fn set_workspace_dir_or_exit() {
             println!("Not in anchor workspace.");
             std::process::exit(1);
         }
-        Some((cfg, _inside_cargo)) => {
+        Some(cfg) => {
             match cfg.path().parent() {
                 None => {
                     println!("Unable to make new program");
@@ -1938,7 +1902,7 @@ fn cluster(_cmd: ClusterCommand) -> Result<()> {
 }
 
 fn shell(cfg_override: &ConfigOverride) -> Result<()> {
-    with_workspace(cfg_override, |cfg, _cargo| {
+    with_workspace(cfg_override, |cfg| {
         let programs = {
             // Create idl map from all workspace programs.
             let mut idls: HashMap<String, Idl> = cfg
@@ -2005,7 +1969,7 @@ fn shell(cfg_override: &ConfigOverride) -> Result<()> {
 }
 
 fn run(cfg_override: &ConfigOverride, script: String) -> Result<()> {
-    with_workspace(cfg_override, |cfg, _cargo| {
+    with_workspace(cfg_override, |cfg| {
         let script = cfg
             .scripts
             .get(&script)
@@ -2040,9 +2004,21 @@ fn login(_cfg_override: &ConfigOverride, token: String) -> Result<()> {
 
 fn publish(cfg_override: &ConfigOverride, program_name: String) -> Result<()> {
     // Discover the various workspace configs.
-    let (cfg, _cargo_path) = Config::discover(cfg_override)?.expect("Not in workspace.");
+    let cfg = Config::discover(cfg_override)?.expect("Not in workspace.");
+
+    let program = cfg
+        .get_program(&program_name)?
+        .ok_or_else(|| anyhow!("Workspace member not found"))?;
 
-    if !Path::new("Cargo.lock").exists() {
+    let program_cargo_lock = pathdiff::diff_paths(
+        program.path().join("Cargo.lock"),
+        cfg.path().parent().unwrap(),
+    )
+    .ok_or_else(|| anyhow!("Unable to diff Cargo.lock path"))?;
+    let cargo_lock = Path::new("Cargo.lock");
+
+    // There must be a Cargo.lock
+    if !program_cargo_lock.exists() && !cargo_lock.exists() {
         return Err(anyhow!("Cargo.lock must exist for a verifiable build"));
     }
 
@@ -2081,29 +2057,59 @@ fn publish(cfg_override: &ConfigOverride, program_name: String) -> Result<()> {
     let mut tar = tar::Builder::new(enc);
 
     // Files that will always be included if they exist.
+    println!("PACKING: Anchor.toml");
     tar.append_path("Anchor.toml")?;
-    tar.append_path("Cargo.lock")?;
+    if cargo_lock.exists() {
+        println!("PACKING: Cargo.lock");
+        tar.append_path(cargo_lock)?;
+    }
     if Path::new("Cargo.toml").exists() {
+        println!("PACKING: Cargo.toml");
         tar.append_path("Cargo.toml")?;
     }
     if Path::new("LICENSE").exists() {
+        println!("PACKING: LICENSE");
         tar.append_path("LICENSE")?;
     }
     if Path::new("README.md").exists() {
+        println!("PACKING: README.md");
         tar.append_path("README.md")?;
     }
 
     // All workspace programs.
     for path in cfg.get_program_list()? {
-        let mut relative_path = pathdiff::diff_paths(path, cfg.path().parent().unwrap())
-            .ok_or_else(|| anyhow!("Unable to diff paths"))?;
-
-        // HACK for workspaces wtih single programs. Change this.
-        if relative_path.display().to_string() == *"" {
-            relative_path = "src".into();
+        let mut dirs = walkdir::WalkDir::new(&path)
+            .into_iter()
+            .filter_entry(|e| !is_hidden(e));
+
+        // Skip the parent dir.
+        let _ = dirs.next().unwrap()?;
+
+        for entry in dirs {
+            let e = entry.map_err(|e| anyhow!("{:?}", e))?;
+
+            let e = pathdiff::diff_paths(e.path(), cfg.path().parent().unwrap())
+                .ok_or_else(|| anyhow!("Unable to diff paths"))?;
+
+            let path_str = e.display().to_string();
+
+            // Skip target dir.
+            if !path_str.contains("target/") && !path_str.contains("/target") {
+                // Only add the file if it's not empty.
+                let metadata = std::fs::File::open(&e)?.metadata()?;
+                if metadata.len() > 0 {
+                    println!("PACKING: {}", e.display().to_string());
+                    if e.is_dir() {
+                        tar.append_dir_all(&e, &e)?;
+                    } else {
+                        tar.append_path(&e)?;
+                    }
+                }
+            }
         }
-        tar.append_dir_all(relative_path.clone(), relative_path)?;
     }
+
+    // Tar pack complete.
     tar.into_inner()?;
 
     // Upload the tarball to the server.
@@ -2158,22 +2164,27 @@ fn registry_api_token(_cfg_override: &ConfigOverride) -> Result<String> {
 //
 // 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<R>(
-    cfg_override: &ConfigOverride,
-    f: impl FnOnce(&WithPath<Config>, Option<PathBuf>) -> R,
-) -> R {
+fn with_workspace<R>(cfg_override: &ConfigOverride, f: impl FnOnce(&WithPath<Config>) -> R) -> R {
     set_workspace_dir_or_exit();
 
     clear_program_keys(cfg_override).unwrap();
 
-    let (cfg, cargo_toml) = Config::discover(cfg_override)
+    let cfg = Config::discover(cfg_override)
         .expect("Previously set the workspace dir")
         .expect("Anchor.toml must always exist");
 
-    let r = f(&cfg, cargo_toml);
+    let r = f(&cfg);
 
     set_workspace_dir_or_exit();
     clear_program_keys(cfg_override).unwrap();
 
     r
 }
+
+fn is_hidden(entry: &walkdir::DirEntry) -> bool {
+    entry
+        .file_name()
+        .to_str()
+        .map(|s| s == "." || s.starts_with('.') || s == "target")
+        .unwrap_or(false)
+}

+ 3 - 3
docs/src/getting-started/publishing.md

@@ -40,7 +40,7 @@ cluster = "mainnet"
 wallet = "~/.config/solana/id.json"
 
 [programs.mainnet]
-multisig = { address = "A9HAbnCwoD6f2NkZobKFf6buJoN9gUVVvX5PoUnDHS6u", path = "./target/deploy/multisig.so", idl = "./target/idl/multisig.json" }
+multisig = "A9HAbnCwoD6f2NkZobKFf6buJoN9gUVVvX5PoUnDHS6u"
 ```
 
 Here there are four sections.
@@ -53,8 +53,8 @@ Here there are four sections.
    standard Anchor workflow, this can be ommitted.  For programs not written in Anchor
    but still want to publish, this should be added.
 3. `[provider]` - configures the wallet and cluster settings. Here, `mainnet` is used because the registry only supports `mainnet` binary verification at the moment.
-3. `[programs.mainnet]` - configures each program in the workpace. Here the
-   `address` of the program to verify and the `path` to it's binary build artifact. For Anchor programs with an **IDL**, an `idl = "<path>"` field should also be provided.
+3. `[programs.mainnet]` - configures each program in the workpace, providing
+   the `address` of the program to verify.
 
 ::: tip
 When defining program in `[programs.mainnet]`, make sure the name provided