use {
anyhow::{anyhow, Context, Result},
serde::Deserialize,
solana_keypair::read_keypair_file,
solana_pubkey::Pubkey,
solana_signer::Signer,
std::{
collections::BTreeMap,
path::{Path, PathBuf},
process::Command,
str::FromStr,
},
};
pub struct LooseWorkspace {
pub root: PathBuf,
pub cwd: PathBuf,
pub current_package: Option<String>,
}
#[derive(Deserialize)]
struct CargoToml {
#[serde(default)]
package: Option<PackageSection>,
#[serde(default)]
workspace: Option<WorkspaceSection>,
#[serde(default)]
features: BTreeMap<String, Vec<String>>,
#[serde(default)]
#[serde(rename = "dev-dependencies")]
dev_dependencies: BTreeMap<String, toml::Value>,
}
#[derive(Deserialize)]
struct PackageSection {
name: String,
}
#[derive(Deserialize)]
struct WorkspaceSection {}
impl LooseWorkspace {
pub fn discover(cwd: PathBuf) -> Result<Self> {
let root = find_workspace_root(&cwd).ok_or_else(|| {
anyhow!(
"no cargo workspace found at or above {} — `anchor debugger` needs either an \
Anchor.toml or a cargo `[workspace]` Cargo.toml",
cwd.display()
)
})?;
let current_package = read_cargo_toml(&cwd.join("Cargo.toml"))
.ok()
.and_then(|m| m.package.map(|p| p.name));
Ok(Self {
root,
cwd,
current_package,
})
}
pub fn cargo_invocation_dir(&self) -> &Path {
if self.current_package.is_some() {
&self.cwd
} else {
&self.root
}
}
pub fn detect_profile_feature(&self) -> Result<String> {
let pkg_manifest = self.cwd.join("Cargo.toml");
let manifest = read_cargo_toml(&pkg_manifest)
.with_context(|| format!("read {}", pkg_manifest.display()))?;
let convention = "profile";
if manifest
.features
.get(convention)
.is_some_and(|v| v.iter().any(|s| s == "anchor-v2-testing/profile"))
{
return Ok(convention.to_owned());
}
for (name, deps) in &manifest.features {
if deps.iter().any(|s| s == "anchor-v2-testing/profile") {
return Ok(name.clone());
}
}
Err(anyhow!(
"no cargo feature in {pkg} forwards to `anchor-v2-testing/profile`.\n\nAdd this to \
{manifest_path}:\n [features]\n profile = [\"anchor-v2-testing/profile\"]\n\nTests \
don't need any cfg gates — `anchor_v2_testing::svm()` is\n`LiteSVM::new()` by \
default and switches to the trace-recording\nvariant automatically when this feature \
is on.",
pkg = self
.current_package
.as_deref()
.unwrap_or("the current crate"),
manifest_path = pkg_manifest.display(),
))
}
pub fn check_dev_dep(&self) -> Result<()> {
let pkg_manifest = self.cwd.join("Cargo.toml");
let manifest = read_cargo_toml(&pkg_manifest)
.with_context(|| format!("read {}", pkg_manifest.display()))?;
if !manifest.dev_dependencies.contains_key("anchor-v2-testing") {
return Err(anyhow!(
"{} doesn't list `anchor-v2-testing` as a dev-dependency.\nAdd it under \
[dev-dependencies] before running `anchor debugger`.",
pkg_manifest.display()
));
}
Ok(())
}
}
fn find_workspace_root(start: &Path) -> Option<PathBuf> {
let mut cur: PathBuf = start.to_path_buf();
loop {
let manifest = cur.join("Cargo.toml");
if manifest.is_file() {
if let Ok(parsed) = read_cargo_toml(&manifest) {
if parsed.workspace.is_some() {
return Some(cur);
}
}
}
if !cur.pop() {
return None;
}
}
}
fn read_cargo_toml(path: &Path) -> Result<CargoToml> {
let contents =
std::fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
toml::from_str(&contents).with_context(|| format!("parse {}", path.display()))
}
pub fn discover_programs(
workspace_root: &Path,
current_package: Option<&str>,
) -> Result<BTreeMap<String, PathBuf>> {
let lib_to_declare_id = scan_declare_ids(workspace_root);
let mut lib_to_so: BTreeMap<String, PathBuf> = BTreeMap::new();
let deploy_dir = workspace_root.join("target").join("deploy");
if deploy_dir.is_dir() {
collect_so_paths(&deploy_dir, &mut lib_to_so);
}
let sbf_release = workspace_root
.join("target")
.join("sbpf-solana-solana")
.join("release");
if sbf_release.is_dir() {
collect_so_paths_if_missing(&sbf_release, &mut lib_to_so);
}
let mut out: BTreeMap<String, PathBuf> = BTreeMap::new();
for (lib_name, so_path) in &lib_to_so {
let mut pubkeys: Vec<String> = Vec::with_capacity(2);
if let Some(pk) = lib_to_declare_id.get(lib_name) {
pubkeys.push(pk.clone());
}
let keypair_path = deploy_dir.join(format!("{lib_name}-keypair.json"));
if keypair_path.is_file() {
if let Ok(kp) = read_keypair_file(&keypair_path) {
pubkeys.push(kp.pubkey().to_string());
}
}
if pubkeys.is_empty() {
if let Ok(pk) = Pubkey::from_str(lib_name) {
pubkeys.push(pk.to_string());
}
}
if pubkeys.is_empty() {
continue;
}
let is_current = current_package
.map(|pkg| lib_name.replace('-', "_") == pkg.replace('-', "_"))
.unwrap_or(false);
for pk in pubkeys {
if is_current {
out.insert(pk, so_path.clone());
} else {
out.entry(pk).or_insert_with(|| so_path.clone());
}
}
}
Ok(out)
}
fn collect_so_paths(dir: &Path, out: &mut BTreeMap<String, PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) != Some("so") {
continue;
}
if path.metadata().map(|m| m.len() == 0).unwrap_or(true) {
continue;
}
let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
continue;
};
out.insert(stem.to_owned(), path);
}
}
fn collect_so_paths_if_missing(dir: &Path, out: &mut BTreeMap<String, PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) != Some("so") {
continue;
}
if path.metadata().map(|m| m.len() == 0).unwrap_or(true) {
continue;
}
let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
continue;
};
out.entry(stem.to_owned()).or_insert(path);
}
}
fn scan_declare_ids(workspace_root: &Path) -> BTreeMap<String, String> {
let mut out = BTreeMap::new();
walk_declare_ids(workspace_root, &mut out, 0);
out
}
fn walk_declare_ids(dir: &Path, out: &mut BTreeMap<String, String>, depth: u8) {
if depth > 8 {
return;
}
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
continue;
};
if matches!(name, "target" | "node_modules" | ".git") || name.starts_with('.') {
continue;
}
walk_declare_ids(&path, out, depth + 1);
} else if path.file_name() == Some(std::ffi::OsStr::new("lib.rs"))
|| path.file_name() == Some(std::ffi::OsStr::new("main.rs"))
{
if let Some((lib_name, pubkey)) = extract_id_pair(&path) {
out.entry(lib_name).or_insert(pubkey);
}
}
}
}
fn extract_id_pair(src_file: &Path) -> Option<(String, String)> {
let contents = std::fs::read_to_string(src_file).ok()?;
let pubkey = find_declare_id(&contents)?;
let mut cur = src_file.parent()?;
loop {
let manifest = cur.join("Cargo.toml");
if manifest.is_file() {
let Ok(parsed) = read_cargo_toml(&manifest) else {
return None;
};
let lib_name = lib_name_from_manifest(&manifest)
.or_else(|| parsed.package.map(|p| p.name.replace('-', "_")))?;
return Some((lib_name, pubkey));
}
cur = cur.parent()?;
}
}
fn lib_name_from_manifest(manifest: &Path) -> Option<String> {
let s = std::fs::read_to_string(manifest).ok()?;
let v: toml::Value = toml::from_str(&s).ok()?;
v.get("lib")?.get("name")?.as_str().map(str::to_owned)
}
fn find_declare_id(src: &str) -> Option<String> {
let idx = src.find("declare_id!")?;
let after = &src[idx + "declare_id!".len()..];
let after = after.trim_start_matches(|c: char| c.is_whitespace());
let after = after.trim_start_matches(['(', '[', '{']);
let quote = after.find('"')?;
let body = &after[quote + 1..];
let end = body.find('"')?;
let id = &body[..end];
if id.len() < 32 || id.len() > 44 {
return None;
}
if !id
.bytes()
.all(|b| b.is_ascii_alphanumeric() && b != b'0' && b != b'O' && b != b'I' && b != b'l')
{
return None;
}
Some(id.to_owned())
}
pub fn run_cargo_test(
cwd: &Path,
package: Option<&str>,
feature: &str,
profile_dir: &Path,
test_filter: Option<&str>,
) -> Result<()> {
let mut cmd = Command::new("cargo");
cmd.current_dir(cwd)
.env("ANCHOR_PROFILE_DIR", profile_dir)
.env("CARGO_PROFILE_RELEASE_DEBUG", "2")
.arg("test")
.arg("--features")
.arg(feature);
if let Some(pkg) = package {
cmd.arg("-p").arg(pkg);
}
if let Some(filter) = test_filter {
cmd.arg("--").arg(filter);
}
let status = cmd
.status()
.with_context(|| format!("spawn cargo test in {}: is `cargo` on PATH?", cwd.display()))?;
if !status.success() {
return Err(anyhow!(
"cargo test failed (exit {:?}). Fix test errors before stepping into the debugger.",
status.code()
));
}
Ok(())
}
pub fn clear_profile_dir(profile_dir: &Path) -> Result<()> {
match std::fs::remove_dir_all(profile_dir) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(anyhow::Error::new(e)
.context(format!("clear stale profile dir {}", profile_dir.display()))),
}
}