1use cargo_metadata::MetadataCommand;
2use once_cell::sync::Lazy;
3use std::{
4 collections::HashMap,
5 env, fmt, fs,
6 path::{Path, PathBuf},
7};
8use strum::{EnumCount, EnumIter, EnumString, EnumVariantNames, IntoEnumIterator};
9
10type CrateNames = HashMap<EthersCrate, &'static str>;
12
13const DIRS: [&str; 3] = ["benches", "examples", "tests"];
14
15static ETHERS_CRATE_NAMES: Lazy<CrateNames> = Lazy::new(|| {
22 ProjectEnvironment::new_from_env()
23 .and_then(|x| x.determine_ethers_crates())
24 .unwrap_or_else(|| EthersCrate::ethers_path_names().collect())
25});
26
27#[inline]
29pub fn ethers_core_crate() -> syn::Path {
30 get_crate_path(EthersCrate::EthersCore)
31}
32
33#[inline]
35pub fn ethers_contract_crate() -> syn::Path {
36 get_crate_path(EthersCrate::EthersContract)
37}
38
39#[inline]
41pub fn ethers_providers_crate() -> syn::Path {
42 get_crate_path(EthersCrate::EthersProviders)
43}
44
45#[inline(always)]
47pub fn get_crate_path(krate: EthersCrate) -> syn::Path {
48 krate.get_path()
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct ProjectEnvironment {
54 manifest_dir: PathBuf,
55 crate_name: Option<String>,
56}
57
58impl ProjectEnvironment {
59 pub fn new<T: Into<PathBuf>, U: Into<String>>(manifest_dir: T, crate_name: U) -> Self {
60 Self { manifest_dir: manifest_dir.into(), crate_name: Some(crate_name.into()) }
61 }
62
63 pub fn new_from_env() -> Option<Self> {
64 Some(Self {
65 manifest_dir: env::var_os("CARGO_MANIFEST_DIR")?.into(),
66 crate_name: env::var("CARGO_CRATE_NAME").ok(),
67 })
68 }
69
70 #[inline]
78 pub fn determine_ethers_crates(&self) -> Option<CrateNames> {
79 let lock_file = self.manifest_dir.join("Cargo.lock");
80 let lock_file_existed = lock_file.exists();
81
82 let names = self.crate_names_from_metadata();
83
84 if !lock_file_existed && lock_file.exists() {
86 let _ = std::fs::remove_file(lock_file);
87 }
88
89 names
90 }
91
92 #[inline]
93 pub fn crate_names_from_metadata(&self) -> Option<CrateNames> {
94 let metadata = MetadataCommand::new().current_dir(&self.manifest_dir).exec().ok()?;
95 let pkg = metadata.root_package()?;
96
97 let crate_is_root = self.is_crate_root();
100 if let Ok(current_pkg) = pkg.name.parse::<EthersCrate>() {
101 let names =
103 EthersCrate::path_names()
104 .map(|(pkg, name)| {
105 if crate_is_root && pkg == current_pkg {
106 (pkg, "crate")
107 } else {
108 (pkg, name)
109 }
110 })
111 .collect();
112 return Some(names)
113 } let mut names: CrateNames = EthersCrate::ethers_path_names().collect();
118 for dep in pkg.dependencies.iter() {
119 let name = dep.name.as_str();
120 if name.starts_with("ethers") {
121 if name == "ethers" {
122 return None
123 } else if let Ok(dep) = name.parse::<EthersCrate>() {
124 names.insert(dep, dep.path_name());
125 }
126 }
127 }
128 Some(names)
129 }
130
131 #[inline]
143 pub fn is_crate_root(&self) -> bool {
144 env::var_os("CARGO_TARGET_TMPDIR").is_none() &&
145 self.manifest_dir.components().all(|c| {
146 let s = c.as_os_str();
147 s != "examples" && s != "benches"
148 }) &&
149 !self.is_crate_name_in_dirs()
150 }
151
152 #[inline]
193 pub fn is_crate_name_in_dirs(&self) -> bool {
194 let crate_name = match self.crate_name.as_ref() {
195 Some(name) => name,
196 None => return false,
197 };
198 let dirs = DIRS.map(|dir| self.manifest_dir.join(dir));
199 dirs.iter().any(|dir| {
200 fs::read_dir(dir)
201 .ok()
202 .and_then(|entries| {
203 entries
204 .filter_map(Result::ok)
205 .find(|entry| file_stem_eq(entry.path(), crate_name))
206 })
207 .is_some()
208 })
209 }
210}
211
212#[derive(
214 Clone,
215 Copy,
216 Debug,
217 PartialEq,
218 Eq,
219 PartialOrd,
220 Ord,
221 Hash,
222 EnumCount,
223 EnumIter,
224 EnumString,
225 EnumVariantNames,
226)]
227#[strum(serialize_all = "kebab-case")]
228pub enum EthersCrate {
229 EthersAddressbook,
230 EthersContract,
231 EthersContractAbigen,
232 EthersContractDerive,
233 EthersCore,
234 EthersDeriveEip712,
235 EthersEtherscan,
236 EthersMiddleware,
237 EthersProviders,
238 EthersSigners,
239 EthersSolc,
240}
241
242impl AsRef<str> for EthersCrate {
243 fn as_ref(&self) -> &str {
244 self.crate_name()
245 }
246}
247
248impl fmt::Display for EthersCrate {
249 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
250 f.pad(self.as_ref())
251 }
252}
253
254impl EthersCrate {
255 #[inline]
257 pub const fn crate_name(self) -> &'static str {
258 match self {
259 Self::EthersAddressbook => "ethers-addressbook",
260 Self::EthersContract => "ethers-contract",
261 Self::EthersContractAbigen => "ethers-contract-abigen",
262 Self::EthersContractDerive => "ethers-contract-derive",
263 Self::EthersCore => "ethers-core",
264 Self::EthersDeriveEip712 => "ethers-derive-eip712",
265 Self::EthersEtherscan => "ethers-etherscan",
266 Self::EthersMiddleware => "ethers-middleware",
267 Self::EthersProviders => "ethers-providers",
268 Self::EthersSigners => "ethers-signers",
269 Self::EthersSolc => "ethers-solc",
270 }
271 }
272
273 #[inline]
275 pub const fn path_name(self) -> &'static str {
276 match self {
277 Self::EthersAddressbook => "::ethers_addressbook",
278 Self::EthersContract => "::ethers_contract",
279 Self::EthersContractAbigen => "::ethers_contract_abigen",
280 Self::EthersContractDerive => "::ethers_contract_derive",
281 Self::EthersCore => "::ethers_core",
282 Self::EthersDeriveEip712 => "::ethers_derive_eip712",
283 Self::EthersEtherscan => "::ethers_etherscan",
284 Self::EthersMiddleware => "::ethers_middleware",
285 Self::EthersProviders => "::ethers_providers",
286 Self::EthersSigners => "::ethers_signers",
287 Self::EthersSolc => "::ethers_solc",
288 }
289 }
290
291 #[inline]
293 pub const fn ethers_path_name(self) -> &'static str {
294 match self {
295 Self::EthersContractAbigen => "::ethers::contract", Self::EthersContractDerive => "::ethers::contract",
298 Self::EthersDeriveEip712 => "::ethers::contract",
299
300 Self::EthersAddressbook => "::ethers::addressbook",
301 Self::EthersContract => "::ethers::contract",
302 Self::EthersCore => "::ethers::core",
303 Self::EthersEtherscan => "::ethers::etherscan",
304 Self::EthersMiddleware => "::ethers::middleware",
305 Self::EthersProviders => "::ethers::providers",
306 Self::EthersSigners => "::ethers::signers",
307 Self::EthersSolc => "::ethers::solc",
308 }
309 }
310
311 #[inline]
313 pub const fn fs_path(self) -> &'static str {
314 match self {
315 Self::EthersContractAbigen => "ethers-contract/ethers-contract-abigen",
316 Self::EthersContractDerive => "ethers-contract/ethers-contract-derive",
317 Self::EthersDeriveEip712 => "ethers-core/ethers-derive-eip712",
318 _ => self.crate_name(),
319 }
320 }
321
322 #[inline]
324 pub fn path_names() -> impl Iterator<Item = (Self, &'static str)> {
325 Self::iter().map(|x| (x, x.path_name()))
326 }
327
328 #[inline]
330 pub fn ethers_path_names() -> impl Iterator<Item = (Self, &'static str)> {
331 Self::iter().map(|x| (x, x.ethers_path_name()))
332 }
333
334 #[inline]
336 pub fn get_path(&self) -> syn::Path {
337 let name = ETHERS_CRATE_NAMES[self];
338 syn::parse_str(name).unwrap()
339 }
340}
341
342#[inline]
344fn file_stem_eq<T: AsRef<Path>, U: AsRef<str>>(path: T, s: U) -> bool {
345 if let Some(stem) = path.as_ref().file_stem() {
346 if let Some(stem) = stem.to_str() {
347 return stem == s.as_ref()
348 }
349 }
350 false
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356 use rand::{
357 distributions::{Distribution, Standard},
358 thread_rng, Rng,
359 };
360 use std::{
361 collections::{BTreeMap, HashSet},
362 env, fs,
363 };
364 use tempfile::TempDir;
365
366 impl Distribution<EthersCrate> for Standard {
367 fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> EthersCrate {
368 const RANGE: std::ops::Range<u8> = 0..EthersCrate::COUNT as u8;
369 unsafe { std::mem::transmute(rng.gen_range(RANGE)) }
371 }
372 }
373
374 #[test]
375 fn test_names() {
376 fn assert_names(s: &ProjectEnvironment, ethers: bool, dependencies: &[EthersCrate]) {
377 write_manifest(s, ethers, dependencies);
378
379 std::fs::write(s.manifest_dir.join("Cargo.lock"), "").unwrap();
382
383 let names = s
384 .determine_ethers_crates()
385 .unwrap_or_else(|| EthersCrate::ethers_path_names().collect());
386
387 let krate = s.crate_name.as_ref().and_then(|x| x.parse::<EthersCrate>().ok());
388 let is_internal = krate.is_some();
389 let mut expected: CrateNames = match (is_internal, ethers) {
390 (true, _) => EthersCrate::path_names().collect(),
392
393 (_, true) => EthersCrate::ethers_path_names().collect(),
395
396 (_, false) => {
398 let mut n: CrateNames = EthersCrate::ethers_path_names().collect();
399 for &dep in dependencies {
400 n.insert(dep, dep.path_name());
401 }
402 n
403 }
404 };
405
406 if is_internal {
407 expected.insert(krate.unwrap(), "crate");
408 }
409
410 if names != expected {
412 let names: BTreeMap<_, _> = names.into_iter().collect();
414 let expected: BTreeMap<_, _> = expected.into_iter().collect();
415 panic!("\nCase failed: (`{:?}`, `{ethers}`, `{dependencies:?}`)\nNames: {names:#?}\nExpected: {expected:#?}\n", s.crate_name);
416 }
417 }
418
419 fn gen_unique<const N: usize>() -> [EthersCrate; N] {
420 assert!(N < EthersCrate::COUNT);
421 let rng = &mut thread_rng();
422 let mut set = HashSet::with_capacity(N);
423 while set.len() < N {
424 set.insert(rng.gen());
425 }
426 let vec: Vec<_> = set.into_iter().collect();
427 vec.try_into().unwrap()
428 }
429
430 let (s, _dir) = test_project();
431 for name in [s.crate_name.as_ref().unwrap(), "ethers-contract"] {
434 let s = ProjectEnvironment::new(&s.manifest_dir, name);
435 assert_names(&s, true, &[]);
437
438 assert_names(&s, false, gen_unique::<3>().as_slice());
440
441 assert_names(&s, true, gen_unique::<3>().as_slice());
443 }
444 }
445
446 #[test]
447 fn test_lock_file() {
448 let (s, _dir) = test_project();
449 write_manifest(&s, true, &[]);
450 let lock_file = s.manifest_dir.join("Cargo.lock");
451
452 assert!(!lock_file.exists());
453 s.determine_ethers_crates();
454 assert!(!lock_file.exists());
455
456 std::fs::write(&lock_file, "").unwrap();
457
458 assert!(lock_file.exists());
459 s.determine_ethers_crates();
460 assert!(lock_file.exists());
461 assert!(!std::fs::read(lock_file).unwrap().is_empty());
462 }
463
464 #[test]
465 fn test_is_crate_root() {
466 let (s, _dir) = test_project();
467 assert!(s.is_crate_root());
468
469 let s = ProjectEnvironment::new(
473 s.manifest_dir.join("examples/complex_examples"),
474 "complex-examples",
475 );
476 assert!(!s.is_crate_root());
477 let s = ProjectEnvironment::new(
478 s.manifest_dir.join("benches/complex_benches"),
479 "complex-benches",
480 );
481 assert!(!s.is_crate_root());
482 }
483
484 #[test]
485 fn test_is_crate_name_in_dirs() {
486 let (s, _dir) = test_project();
487 let root = &s.manifest_dir;
488
489 for dir_name in DIRS {
490 for ty in ["simple", "complex"] {
491 let s = ProjectEnvironment::new(root, format!("{ty}_{dir_name}"));
492 assert!(s.is_crate_name_in_dirs(), "{s:?}");
493 }
494 }
495
496 let s = ProjectEnvironment::new(root, "non_existant");
497 assert!(!s.is_crate_name_in_dirs());
498 let s = ProjectEnvironment::new(root.join("does-not-exist"), "foo_bar");
499 assert!(!s.is_crate_name_in_dirs());
500 }
501
502 #[test]
503 fn test_file_stem_eq() {
504 let path = Path::new("/tmp/foo.rs");
505 assert!(file_stem_eq(path, "foo"));
506 assert!(!file_stem_eq(path, "tmp"));
507 assert!(!file_stem_eq(path, "foo.rs"));
508 assert!(!file_stem_eq(path, "fo"));
509 assert!(!file_stem_eq(path, "f"));
510 assert!(!file_stem_eq(path, ""));
511
512 let path = Path::new("/tmp/foo/");
513 assert!(file_stem_eq(path, "foo"));
514 assert!(!file_stem_eq(path, "tmp"));
515 assert!(!file_stem_eq(path, "fo"));
516 assert!(!file_stem_eq(path, "f"));
517 assert!(!file_stem_eq(path, ""));
518 }
519
520 fn test_project() -> (ProjectEnvironment, TempDir) {
536 let dir = tempfile::Builder::new().prefix("tmp").tempdir().unwrap();
538 let root = dir.path();
539 let name = root.file_name().unwrap().to_str().unwrap();
540
541 fs::create_dir_all(root).unwrap();
543 let src = root.join("src");
544 fs::create_dir(&src).unwrap();
545 fs::write(src.join("main.rs"), "fn main(){}").unwrap();
546
547 for dir_name in DIRS {
548 let new_dir = root.join(dir_name);
549 fs::create_dir(&new_dir).unwrap();
550
551 let simple = new_dir.join(format!("simple_{dir_name}.rs"));
552 fs::write(simple, "").unwrap();
553
554 let mut complex = new_dir.join(format!("complex_{dir_name}"));
555 if dir_name != "tests" {
556 fs::create_dir(&complex).unwrap();
557 fs::write(complex.join("Cargo.toml"), "").unwrap();
558 complex.push("src");
559 }
560 fs::create_dir(&complex).unwrap();
561 fs::write(complex.join("main.rs"), "").unwrap();
562 fs::write(complex.join("module.rs"), "").unwrap();
563 }
564
565 let target = root.join("target");
567 fs::create_dir(&target).unwrap();
568 fs::create_dir_all(target.join("tmp")).unwrap();
569
570 (ProjectEnvironment::new(root, name), dir)
571 }
572
573 fn write_manifest(s: &ProjectEnvironment, ethers: bool, dependencies: &[EthersCrate]) {
575 const ETHERS_CORE: &str = env!("CARGO_MANIFEST_DIR");
577 let ethers_root = Path::new(ETHERS_CORE).parent().unwrap();
578 let mut dependencies_toml =
579 String::with_capacity(150 * (ethers as usize + dependencies.len()));
580
581 if ethers {
582 let ethers = format!("ethers = {{ path = {ethers_root:?} }}\n");
583 dependencies_toml.push_str(ðers);
584 }
585
586 for dep in dependencies.iter() {
587 let path = ethers_root.join(dep.fs_path());
588 let dep = format!("{dep} = {{ path = {path:?} }}\n");
589 dependencies_toml.push_str(&dep);
590 }
591
592 let contents = format!(
593 r#"
594[package]
595name = "{}"
596version = "0.0.0"
597edition = "2021"
598
599[dependencies]
600{dependencies_toml}
601"#,
602 s.crate_name.as_ref().unwrap()
603 );
604 fs::write(s.manifest_dir.join("Cargo.toml"), contents).unwrap();
605 }
606}