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

This commit is contained in:
Armani Ferrante 2021-08-10 21:36:56 -07:00 committed by GitHub
parent 80e4030e45
commit bd68e28f15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 279 additions and 204 deletions

View File

@ -15,6 +15,10 @@ incremented for features.
* cli: Programs embedded into genesis during tests will produce program logs. * 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 ## [0.13.0] - 2021-08-08
### Features ### Features

1
Cargo.lock generated
View File

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

View File

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

View File

@ -9,6 +9,7 @@ use std::collections::BTreeMap;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::prelude::*; use std::io::prelude::*;
use std::ops::Deref;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; 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> { impl WithPath<Config> {
pub fn get_program_list(&self) -> Result<Vec<PathBuf>> { pub fn get_program_list(&self) -> Result<Vec<PathBuf>> {
// Canonicalize the workspace filepaths to compare with relative paths. // Canonicalize the workspace filepaths to compare with relative paths.
@ -82,7 +145,7 @@ impl WithPath<Config> {
let mut r = vec![]; let mut r = vec![];
for path in self.get_program_list()? { for path in self.get_program_list()? {
let idl = anchor_syn::idl::file::parse(path.join("src/lib.rs"))?; 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 { r.push(Program {
lib_name, lib_name,
path, path,
@ -97,16 +160,53 @@ impl WithPath<Config> {
.workspace .workspace
.members .members
.iter() .iter()
.map(|m| PathBuf::from(m).canonicalize().unwrap()) .map(|m| {
self.path()
.parent()
.unwrap()
.join(m)
.canonicalize()
.unwrap()
})
.collect(); .collect();
let exclude = self let exclude = self
.workspace .workspace
.exclude .exclude
.iter() .iter()
.map(|m| PathBuf::from(m).canonicalize().unwrap()) .map(|m| {
self.path()
.parent()
.unwrap()
.join(m)
.canonicalize()
.unwrap()
})
.collect(); .collect();
Ok((members, exclude)) 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> { impl<T> std::ops::Deref for WithPath<T> {
@ -174,71 +274,51 @@ impl Config {
format!("projectserum/build:v{}", ver) format!("projectserum/build:v{}", ver)
} }
pub fn discover( pub fn discover(cfg_override: &ConfigOverride) -> Result<Option<WithPath<Config>>> {
cfg_override: &ConfigOverride,
) -> Result<Option<(WithPath<Config>, Option<PathBuf>)>> {
Config::_discover().map(|opt| { Config::_discover().map(|opt| {
opt.map(|(mut cfg, cargo_toml)| { opt.map(|mut cfg| {
if let Some(cluster) = cfg_override.cluster.clone() { if let Some(cluster) = cfg_override.cluster.clone() {
cfg.provider.cluster = cluster; cfg.provider.cluster = cluster;
} }
if let Some(wallet) = cfg_override.wallet.clone() { if let Some(wallet) = cfg_override.wallet.clone() {
cfg.provider.wallet = wallet; cfg.provider.wallet = wallet;
} }
(cfg, cargo_toml) cfg
}) })
}) })
} }
// Searches all parent directories for an Anchor.toml file. // Climbs each parent directory until we find an Anchor.toml.
fn _discover() -> Result<Option<(WithPath<Config>, Option<PathBuf>)>> { fn _discover() -> Result<Option<WithPath<Config>>> {
// Set to true if we ever see a Cargo.toml file when traversing the
// parent directories.
let mut cargo_toml = None;
let _cwd = std::env::current_dir()?; let _cwd = std::env::current_dir()?;
let mut cwd_opt = Some(_cwd.as_path()); let mut cwd_opt = Some(_cwd.as_path());
while let Some(cwd) = cwd_opt { while let Some(cwd) = cwd_opt {
let files = fs::read_dir(cwd)?; for f in 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 {
let p = f?.path(); let p = f?.path();
if let Some(filename) = p.file_name() { if let Some(filename) = p.file_name() {
if filename.to_str() == Some("Cargo.toml") { if filename.to_str() == Some("Anchor.toml") {
cargo_toml_level = Some(p); let cfg = Config::from_path(&p)?;
} else if filename.to_str() == Some("Anchor.toml") { return Ok(Some(WithPath::new(cfg, p)));
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));
} }
} }
} }
// 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(); cwd_opt = cwd.parent();
} }
Ok(None) 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> { pub fn wallet_kp(&self) -> Result<Keypair> {
solana_sdk::signature::read_keypair_file(&self.provider.wallet.to_string()) solana_sdk::signature::read_keypair_file(&self.provider.wallet.to_string())
.map_err(|_| anyhow!("Unable to read keypair file")) .map_err(|_| anyhow!("Unable to read keypair file"))
@ -382,21 +462,10 @@ pub struct GenesisEntry {
pub program: String, 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)] #[derive(Debug, Clone)]
pub struct Program { pub struct Program {
pub lib_name: String, pub lib_name: String,
// Canonicalized path to the program directory.
pub path: PathBuf, pub path: PathBuf,
pub idl: Option<Idl>, pub idl: Option<Idl>,
} }
@ -463,7 +532,6 @@ pub struct ProgramWorkspace {
pub struct AnchorPackage { pub struct AnchorPackage {
pub name: String, pub name: String,
pub address: String, pub address: String,
pub path: String,
pub idl: Option<String>, pub idl: Option<String>,
} }
@ -479,19 +547,9 @@ impl AnchorPackage {
.ok_or_else(|| anyhow!("Program not provided in Anchor.toml"))? .ok_or_else(|| anyhow!("Program not provided in Anchor.toml"))?
.get(&name) .get(&name)
.ok_or_else(|| anyhow!("Program not provided in Anchor.toml"))?; .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 idl = program_details.idl.clone();
let address = program_details.address.to_string(); let address = program_details.address.to_string();
Ok(Self { Ok(Self { name, address, idl })
name,
path,
address,
idl,
})
} }
} }

View File

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

View File

@ -40,7 +40,7 @@ cluster = "mainnet"
wallet = "~/.config/solana/id.json" wallet = "~/.config/solana/id.json"
[programs.mainnet] [programs.mainnet]
multisig = { address = "A9HAbnCwoD6f2NkZobKFf6buJoN9gUVVvX5PoUnDHS6u", path = "./target/deploy/multisig.so", idl = "./target/idl/multisig.json" } multisig = "A9HAbnCwoD6f2NkZobKFf6buJoN9gUVVvX5PoUnDHS6u"
``` ```
Here there are four sections. 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 standard Anchor workflow, this can be ommitted. For programs not written in Anchor
but still want to publish, this should be added. 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. `[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 3. `[programs.mainnet]` - configures each program in the workpace, providing
`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. the `address` of the program to verify.
::: tip ::: tip
When defining program in `[programs.mainnet]`, make sure the name provided When defining program in `[programs.mainnet]`, make sure the name provided