use cargo_metadata::MetadataCommand;
use once_cell::sync::Lazy;
use std::{
collections::HashMap,
env, fmt, fs,
path::{Path, PathBuf},
};
use strum::{EnumCount, EnumIter, EnumString, IntoEnumIterator, VariantNames};
type CrateNames = HashMap<EthersCrate, &'static str>;
const DIRS: [&str; 3] = ["benches", "examples", "tests"];
static ETHERS_CRATE_NAMES: Lazy<CrateNames> = Lazy::new(|| {
ProjectEnvironment::new_from_env()
.and_then(|x| x.determine_ethers_crates())
.unwrap_or_else(|| EthersCrate::ethers_path_names().collect())
});
#[inline]
pub fn ethers_core_crate() -> syn::Path {
get_crate_path(EthersCrate::EthersCore)
}
#[inline]
pub fn ethers_contract_crate() -> syn::Path {
get_crate_path(EthersCrate::EthersContract)
}
#[inline]
pub fn ethers_providers_crate() -> syn::Path {
get_crate_path(EthersCrate::EthersProviders)
}
#[inline(always)]
pub fn get_crate_path(krate: EthersCrate) -> syn::Path {
krate.get_path()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProjectEnvironment {
manifest_dir: PathBuf,
crate_name: Option<String>,
}
impl ProjectEnvironment {
pub fn new<T: Into<PathBuf>, U: Into<String>>(manifest_dir: T, crate_name: U) -> Self {
Self { manifest_dir: manifest_dir.into(), crate_name: Some(crate_name.into()) }
}
pub fn new_from_env() -> Option<Self> {
Some(Self {
manifest_dir: env::var_os("CARGO_MANIFEST_DIR")?.into(),
crate_name: env::var("CARGO_CRATE_NAME").ok(),
})
}
#[inline]
pub fn determine_ethers_crates(&self) -> Option<CrateNames> {
let lock_file = self.manifest_dir.join("Cargo.lock");
let lock_file_existed = lock_file.exists();
let names = self.crate_names_from_metadata();
if !lock_file_existed && lock_file.exists() {
let _ = std::fs::remove_file(lock_file);
}
names
}
#[inline]
fn crate_names_from_metadata(&self) -> Option<CrateNames> {
let metadata = MetadataCommand::new().current_dir(&self.manifest_dir).exec().ok()?;
let pkg = metadata.root_package()?;
if pkg.name.parse::<EthersCrate>().is_ok() || pkg.name == "ethers" {
return Some(EthersCrate::path_names().collect())
}
let mut names: CrateNames = EthersCrate::ethers_path_names().collect();
for dep in pkg.dependencies.iter() {
let name = dep.name.as_str();
if name.starts_with("ethers") {
if name == "ethers" {
return None
} else if let Ok(dep) = name.parse::<EthersCrate>() {
names.insert(dep, dep.path_name());
}
}
}
Some(names)
}
#[inline]
pub fn is_crate_root(&self) -> bool {
env::var_os("CARGO_TARGET_TMPDIR").is_none() &&
self.manifest_dir.components().all(|c| {
let s = c.as_os_str();
s != "examples" && s != "benches"
}) &&
!self.is_crate_name_in_dirs()
}
#[inline]
pub fn is_crate_name_in_dirs(&self) -> bool {
let crate_name = match self.crate_name.as_ref() {
Some(name) => name,
None => return false,
};
let dirs = DIRS.map(|dir| self.manifest_dir.join(dir));
dirs.iter().any(|dir| {
fs::read_dir(dir)
.ok()
.and_then(|entries| {
entries
.filter_map(Result::ok)
.find(|entry| file_stem_eq(entry.path(), crate_name))
})
.is_some()
})
}
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
EnumCount,
EnumIter,
EnumString,
VariantNames,
)]
#[strum(serialize_all = "kebab-case")]
pub enum EthersCrate {
EthersAddressbook,
EthersContract,
EthersContractAbigen,
EthersContractDerive,
EthersCore,
EthersEtherscan,
EthersMiddleware,
EthersProviders,
EthersSigners,
EthersSolc,
}
impl AsRef<str> for EthersCrate {
fn as_ref(&self) -> &str {
self.crate_name()
}
}
impl fmt::Display for EthersCrate {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.pad(self.as_ref())
}
}
impl EthersCrate {
#[inline]
pub const fn crate_name(self) -> &'static str {
match self {
Self::EthersAddressbook => "ethers-addressbook",
Self::EthersContract => "ethers-contract",
Self::EthersContractAbigen => "ethers-contract-abigen",
Self::EthersContractDerive => "ethers-contract-derive",
Self::EthersCore => "ethers-core",
Self::EthersEtherscan => "ethers-etherscan",
Self::EthersMiddleware => "ethers-middleware",
Self::EthersProviders => "ethers-providers",
Self::EthersSigners => "ethers-signers",
Self::EthersSolc => "ethers-solc",
}
}
#[inline]
pub const fn path_name(self) -> &'static str {
match self {
Self::EthersAddressbook => "::ethers_addressbook",
Self::EthersContract => "::ethers_contract",
Self::EthersContractAbigen => "::ethers_contract_abigen",
Self::EthersContractDerive => "::ethers_contract_derive",
Self::EthersCore => "::ethers_core",
Self::EthersEtherscan => "::ethers_etherscan",
Self::EthersMiddleware => "::ethers_middleware",
Self::EthersProviders => "::ethers_providers",
Self::EthersSigners => "::ethers_signers",
Self::EthersSolc => "::ethers_solc",
}
}
#[inline]
pub const fn ethers_path_name(self) -> &'static str {
match self {
Self::EthersContractAbigen => "::ethers::contract", Self::EthersContractDerive => "::ethers::contract",
Self::EthersAddressbook => "::ethers::addressbook",
Self::EthersContract => "::ethers::contract",
Self::EthersCore => "::ethers::core",
Self::EthersEtherscan => "::ethers::etherscan",
Self::EthersMiddleware => "::ethers::middleware",
Self::EthersProviders => "::ethers::providers",
Self::EthersSigners => "::ethers::signers",
Self::EthersSolc => "::ethers::solc",
}
}
#[inline]
pub const fn fs_path(self) -> &'static str {
match self {
Self::EthersContractAbigen => "ethers-contract/ethers-contract-abigen",
Self::EthersContractDerive => "ethers-contract/ethers-contract-derive",
_ => self.crate_name(),
}
}
#[inline]
pub fn path_names() -> impl Iterator<Item = (Self, &'static str)> {
Self::iter().map(|x| (x, x.path_name()))
}
#[inline]
pub fn ethers_path_names() -> impl Iterator<Item = (Self, &'static str)> {
Self::iter().map(|x| (x, x.ethers_path_name()))
}
#[inline]
pub fn get_path(&self) -> syn::Path {
let name = ETHERS_CRATE_NAMES[self];
syn::parse_str(name).unwrap()
}
}
#[inline]
fn file_stem_eq<T: AsRef<Path>, U: AsRef<str>>(path: T, s: U) -> bool {
if let Some(stem) = path.as_ref().file_stem() {
if let Some(stem) = stem.to_str() {
return stem == s.as_ref()
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use rand::{
distributions::{Distribution, Standard},
thread_rng, Rng,
};
use std::{
collections::{BTreeMap, HashSet},
env,
};
use tempfile::TempDir;
impl Distribution<EthersCrate> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> EthersCrate {
const RANGE: std::ops::Range<u8> = 0..EthersCrate::COUNT as u8;
unsafe { std::mem::transmute(rng.gen_range(RANGE)) }
}
}
#[test]
#[ignore = "TODO: flaky and slow"]
fn test_names() {
fn assert_names(s: &ProjectEnvironment, ethers: bool, dependencies: &[EthersCrate]) {
write_manifest(s, ethers, dependencies);
std::fs::write(s.manifest_dir.join("Cargo.lock"), "").unwrap();
let names = s
.determine_ethers_crates()
.unwrap_or_else(|| EthersCrate::ethers_path_names().collect());
let krate = s.crate_name.as_ref().and_then(|x| x.parse::<EthersCrate>().ok());
let is_internal = krate.is_some();
let expected: CrateNames = match (is_internal, ethers) {
(true, _) => EthersCrate::path_names().collect(),
(_, true) => EthersCrate::ethers_path_names().collect(),
(_, false) => {
let mut n: CrateNames = EthersCrate::ethers_path_names().collect();
for &dep in dependencies {
n.insert(dep, dep.path_name());
}
n
}
};
if names != expected {
let names: BTreeMap<_, _> = names.into_iter().collect();
let expected: BTreeMap<_, _> = expected.into_iter().collect();
panic!("\nCase failed: (`{:?}`, `{ethers}`, `{dependencies:?}`)\nNames: {names:#?}\nExpected: {expected:#?}\n", s.crate_name);
}
}
fn gen_unique<const N: usize>() -> [EthersCrate; N] {
assert!(N < EthersCrate::COUNT);
let rng = &mut thread_rng();
let mut set = HashSet::with_capacity(N);
while set.len() < N {
set.insert(rng.gen());
}
let vec: Vec<_> = set.into_iter().collect();
vec.try_into().unwrap()
}
let (s, _dir) = test_project();
for name in [s.crate_name.as_ref().unwrap(), "ethers-contract"] {
let s = ProjectEnvironment::new(&s.manifest_dir, name);
assert_names(&s, true, &[]);
assert_names(&s, false, gen_unique::<3>().as_slice());
assert_names(&s, true, gen_unique::<3>().as_slice());
}
}
#[test]
#[ignore = "TODO: flaky and slow"]
fn test_lock_file() {
let (s, _dir) = test_project();
write_manifest(&s, true, &[]);
let lock_file = s.manifest_dir.join("Cargo.lock");
assert!(!lock_file.exists());
s.determine_ethers_crates();
assert!(!lock_file.exists());
std::fs::write(&lock_file, "").unwrap();
assert!(lock_file.exists());
s.determine_ethers_crates();
assert!(lock_file.exists());
assert!(!std::fs::read(lock_file).unwrap().is_empty());
}
#[test]
fn test_is_crate_root() {
let (s, _dir) = test_project();
assert!(s.is_crate_root());
let s = ProjectEnvironment::new(
s.manifest_dir.join("examples/complex_examples"),
"complex-examples",
);
assert!(!s.is_crate_root());
let s = ProjectEnvironment::new(
s.manifest_dir.join("benches/complex_benches"),
"complex-benches",
);
assert!(!s.is_crate_root());
}
#[test]
fn test_is_crate_name_in_dirs() {
let (s, _dir) = test_project();
let root = &s.manifest_dir;
for dir_name in DIRS {
for ty in ["simple", "complex"] {
let s = ProjectEnvironment::new(root, format!("{ty}_{dir_name}"));
assert!(s.is_crate_name_in_dirs(), "{s:?}");
}
}
let s = ProjectEnvironment::new(root, "non_existant");
assert!(!s.is_crate_name_in_dirs());
let s = ProjectEnvironment::new(root.join("does-not-exist"), "foo_bar");
assert!(!s.is_crate_name_in_dirs());
}
#[test]
fn test_file_stem_eq() {
let path = Path::new("/tmp/foo.rs");
assert!(file_stem_eq(path, "foo"));
assert!(!file_stem_eq(path, "tmp"));
assert!(!file_stem_eq(path, "foo.rs"));
assert!(!file_stem_eq(path, "fo"));
assert!(!file_stem_eq(path, "f"));
assert!(!file_stem_eq(path, ""));
let path = Path::new("/tmp/foo/");
assert!(file_stem_eq(path, "foo"));
assert!(!file_stem_eq(path, "tmp"));
assert!(!file_stem_eq(path, "fo"));
assert!(!file_stem_eq(path, "f"));
assert!(!file_stem_eq(path, ""));
}
fn test_project() -> (ProjectEnvironment, TempDir) {
let dir = tempfile::Builder::new().prefix("tmp").tempdir().unwrap();
let root = dir.path();
let name = root.file_name().unwrap().to_str().unwrap();
fs::create_dir_all(root).unwrap();
let src = root.join("src");
fs::create_dir(&src).unwrap();
fs::write(src.join("main.rs"), "fn main(){}").unwrap();
for dir_name in DIRS {
let new_dir = root.join(dir_name);
fs::create_dir(&new_dir).unwrap();
let simple = new_dir.join(format!("simple_{dir_name}.rs"));
fs::write(simple, "").unwrap();
let mut complex = new_dir.join(format!("complex_{dir_name}"));
if dir_name != "tests" {
fs::create_dir(&complex).unwrap();
fs::write(complex.join("Cargo.toml"), "").unwrap();
complex.push("src");
}
fs::create_dir(&complex).unwrap();
fs::write(complex.join("main.rs"), "").unwrap();
fs::write(complex.join("module.rs"), "").unwrap();
}
let target = root.join("target");
fs::create_dir(&target).unwrap();
fs::create_dir_all(target.join("tmp")).unwrap();
(ProjectEnvironment::new(root, name), dir)
}
fn write_manifest(s: &ProjectEnvironment, ethers: bool, dependencies: &[EthersCrate]) {
const ETHERS_CORE: &str = env!("CARGO_MANIFEST_DIR");
let ethers_root = Path::new(ETHERS_CORE).parent().unwrap();
let mut dependencies_toml =
String::with_capacity(150 * (ethers as usize + dependencies.len()));
if ethers {
let path = ethers_root.join("ethers");
let ethers = format!("ethers = {{ path = \"{}\" }}\n", path.display());
dependencies_toml.push_str(ðers);
}
for dep in dependencies.iter() {
let path = ethers_root.join(dep.fs_path());
let dep = format!("{dep} = {{ path = \"{}\" }}\n", path.display());
dependencies_toml.push_str(&dep);
}
let contents = format!(
r#"
[package]
name = "{}"
version = "0.0.0"
edition = "2021"
[dependencies]
{dependencies_toml}
"#,
s.crate_name.as_ref().unwrap()
);
fs::write(s.manifest_dir.join("Cargo.toml"), contents).unwrap();
}
}