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, IntoEnumIterator, VariantNames};
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 {
61 Self { manifest_dir: manifest_dir.into(), crate_name: Some(crate_name.into()) }
62 }
63
64 pub fn new_from_env() -> Option<Self> {
67 Some(Self {
68 manifest_dir: env::var_os("CARGO_MANIFEST_DIR")?.into(),
69 crate_name: env::var("CARGO_CRATE_NAME").ok(),
70 })
71 }
72
73 #[inline]
81 pub fn determine_ethers_crates(&self) -> Option<CrateNames> {
82 let lock_file = self.manifest_dir.join("Cargo.lock");
83 let lock_file_existed = lock_file.exists();
84
85 let names = self.crate_names_from_metadata();
86
87 if !lock_file_existed && lock_file.exists() {
89 let _ = std::fs::remove_file(lock_file);
90 }
91
92 names
93 }
94
95 #[inline]
96 fn crate_names_from_metadata(&self) -> Option<CrateNames> {
97 let metadata = MetadataCommand::new().current_dir(&self.manifest_dir).exec().ok()?;
98 let pkg = metadata.root_package()?;
99
100 if pkg.name.parse::<EthersCrate>().is_ok() || pkg.name == "ethers" {
103 return Some(EthersCrate::path_names().collect())
104 }
105
106 let mut names: CrateNames = EthersCrate::ethers_path_names().collect();
107 for dep in pkg.dependencies.iter() {
108 let name = dep.name.as_str();
109 if name.starts_with("ethers") {
110 if name == "ethers" {
111 return None
112 } else if let Ok(dep) = name.parse::<EthersCrate>() {
113 names.insert(dep, dep.path_name());
114 }
115 }
116 }
117 Some(names)
118 }
119
120 #[inline]
132 pub fn is_crate_root(&self) -> bool {
133 env::var_os("CARGO_TARGET_TMPDIR").is_none() &&
134 self.manifest_dir.components().all(|c| {
135 let s = c.as_os_str();
136 s != "examples" && s != "benches"
137 }) &&
138 !self.is_crate_name_in_dirs()
139 }
140
141 #[inline]
182 pub fn is_crate_name_in_dirs(&self) -> bool {
183 let crate_name = match self.crate_name.as_ref() {
184 Some(name) => name,
185 None => return false,
186 };
187 let dirs = DIRS.map(|dir| self.manifest_dir.join(dir));
188 dirs.iter().any(|dir| {
189 fs::read_dir(dir)
190 .ok()
191 .and_then(|entries| {
192 entries
193 .filter_map(Result::ok)
194 .find(|entry| file_stem_eq(entry.path(), crate_name))
195 })
196 .is_some()
197 })
198 }
199}
200
201#[derive(
203 Clone,
204 Copy,
205 Debug,
206 PartialEq,
207 Eq,
208 PartialOrd,
209 Ord,
210 Hash,
211 EnumCount,
212 EnumIter,
213 EnumString,
214 VariantNames,
215)]
216#[strum(serialize_all = "kebab-case")]
217pub enum EthersCrate {
218 EthersAddressbook,
219 EthersContract,
220 EthersContractAbigen,
221 EthersContractDerive,
222 EthersCore,
223 EthersEtherscan,
224 EthersMiddleware,
225 EthersProviders,
226 EthersSigners,
227 EthersSolc,
228}
229
230impl AsRef<str> for EthersCrate {
231 fn as_ref(&self) -> &str {
232 self.crate_name()
233 }
234}
235
236impl fmt::Display for EthersCrate {
237 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
238 f.pad(self.as_ref())
239 }
240}
241
242impl EthersCrate {
243 #[inline]
245 pub const fn crate_name(self) -> &'static str {
246 match self {
247 Self::EthersAddressbook => "ethers-addressbook",
248 Self::EthersContract => "ethers-contract",
249 Self::EthersContractAbigen => "ethers-contract-abigen",
250 Self::EthersContractDerive => "ethers-contract-derive",
251 Self::EthersCore => "ethers-core",
252 Self::EthersEtherscan => "ethers-etherscan",
253 Self::EthersMiddleware => "ethers-middleware",
254 Self::EthersProviders => "ethers-providers",
255 Self::EthersSigners => "ethers-signers",
256 Self::EthersSolc => "ethers-solc",
257 }
258 }
259
260 #[inline]
262 pub const fn path_name(self) -> &'static str {
263 match self {
264 Self::EthersAddressbook => "::ethers_addressbook",
265 Self::EthersContract => "::ethers_contract",
266 Self::EthersContractAbigen => "::ethers_contract_abigen",
267 Self::EthersContractDerive => "::ethers_contract_derive",
268 Self::EthersCore => "::ethers_core",
269 Self::EthersEtherscan => "::ethers_etherscan",
270 Self::EthersMiddleware => "::ethers_middleware",
271 Self::EthersProviders => "::ethers_providers",
272 Self::EthersSigners => "::ethers_signers",
273 Self::EthersSolc => "::ethers_solc",
274 }
275 }
276
277 #[inline]
279 pub const fn ethers_path_name(self) -> &'static str {
280 match self {
281 Self::EthersContractAbigen => "::ethers::contract", Self::EthersContractDerive => "::ethers::contract",
284
285 Self::EthersAddressbook => "::ethers::addressbook",
286 Self::EthersContract => "::ethers::contract",
287 Self::EthersCore => "::ethers::core",
288 Self::EthersEtherscan => "::ethers::etherscan",
289 Self::EthersMiddleware => "::ethers::middleware",
290 Self::EthersProviders => "::ethers::providers",
291 Self::EthersSigners => "::ethers::signers",
292 Self::EthersSolc => "::ethers::solc",
293 }
294 }
295
296 #[inline]
298 pub const fn fs_path(self) -> &'static str {
299 match self {
300 Self::EthersContractAbigen => "ethers-contract/ethers-contract-abigen",
301 Self::EthersContractDerive => "ethers-contract/ethers-contract-derive",
302 _ => self.crate_name(),
303 }
304 }
305
306 #[inline]
308 pub fn path_names() -> impl Iterator<Item = (Self, &'static str)> {
309 Self::iter().map(|x| (x, x.path_name()))
310 }
311
312 #[inline]
314 pub fn ethers_path_names() -> impl Iterator<Item = (Self, &'static str)> {
315 Self::iter().map(|x| (x, x.ethers_path_name()))
316 }
317
318 #[inline]
320 pub fn get_path(&self) -> syn::Path {
321 let name = ETHERS_CRATE_NAMES[self];
322 syn::parse_str(name).unwrap()
323 }
324}
325
326#[inline]
328fn file_stem_eq<T: AsRef<Path>, U: AsRef<str>>(path: T, s: U) -> bool {
329 if let Some(stem) = path.as_ref().file_stem() {
330 if let Some(stem) = stem.to_str() {
331 return stem == s.as_ref()
332 }
333 }
334 false
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340 use rand::{
341 distributions::{Distribution, Standard},
342 thread_rng, Rng,
343 };
344 use std::{
345 collections::{BTreeMap, HashSet},
346 env,
347 };
348 use tempfile::TempDir;
349
350 impl Distribution<EthersCrate> for Standard {
351 fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> EthersCrate {
352 const RANGE: std::ops::Range<u8> = 0..EthersCrate::COUNT as u8;
353 unsafe { std::mem::transmute(rng.gen_range(RANGE)) }
355 }
356 }
357
358 #[test]
359 #[ignore = "TODO: flaky and slow"]
360 fn test_names() {
361 fn assert_names(s: &ProjectEnvironment, ethers: bool, dependencies: &[EthersCrate]) {
362 write_manifest(s, ethers, dependencies);
363
364 std::fs::write(s.manifest_dir.join("Cargo.lock"), "").unwrap();
367
368 let names = s
369 .determine_ethers_crates()
370 .unwrap_or_else(|| EthersCrate::ethers_path_names().collect());
371
372 let krate = s.crate_name.as_ref().and_then(|x| x.parse::<EthersCrate>().ok());
373 let is_internal = krate.is_some();
374 let expected: CrateNames = match (is_internal, ethers) {
375 (true, _) => EthersCrate::path_names().collect(),
377
378 (_, true) => EthersCrate::ethers_path_names().collect(),
380
381 (_, false) => {
383 let mut n: CrateNames = EthersCrate::ethers_path_names().collect();
384 for &dep in dependencies {
385 n.insert(dep, dep.path_name());
386 }
387 n
388 }
389 };
390
391 if names != expected {
393 let names: BTreeMap<_, _> = names.into_iter().collect();
395 let expected: BTreeMap<_, _> = expected.into_iter().collect();
396 panic!("\nCase failed: (`{:?}`, `{ethers}`, `{dependencies:?}`)\nNames: {names:#?}\nExpected: {expected:#?}\n", s.crate_name);
397 }
398 }
399
400 fn gen_unique<const N: usize>() -> [EthersCrate; N] {
401 assert!(N < EthersCrate::COUNT);
402 let rng = &mut thread_rng();
403 let mut set = HashSet::with_capacity(N);
404 while set.len() < N {
405 set.insert(rng.gen());
406 }
407 let vec: Vec<_> = set.into_iter().collect();
408 vec.try_into().unwrap()
409 }
410
411 let (s, _dir) = test_project();
412 for name in [s.crate_name.as_ref().unwrap(), "ethers-contract"] {
415 let s = ProjectEnvironment::new(&s.manifest_dir, name);
416 assert_names(&s, true, &[]);
418
419 assert_names(&s, false, gen_unique::<3>().as_slice());
421
422 assert_names(&s, true, gen_unique::<3>().as_slice());
424 }
425 }
426
427 #[test]
428 #[ignore = "TODO: flaky and slow"]
429 fn test_lock_file() {
430 let (s, _dir) = test_project();
431 write_manifest(&s, true, &[]);
432 let lock_file = s.manifest_dir.join("Cargo.lock");
433
434 assert!(!lock_file.exists());
435 s.determine_ethers_crates();
436 assert!(!lock_file.exists());
437
438 std::fs::write(&lock_file, "").unwrap();
439
440 assert!(lock_file.exists());
441 s.determine_ethers_crates();
442 assert!(lock_file.exists());
443 assert!(!std::fs::read(lock_file).unwrap().is_empty());
444 }
445
446 #[test]
447 fn test_is_crate_root() {
448 let (s, _dir) = test_project();
449 assert!(s.is_crate_root());
450
451 let s = ProjectEnvironment::new(
455 s.manifest_dir.join("examples/complex_examples"),
456 "complex-examples",
457 );
458 assert!(!s.is_crate_root());
459 let s = ProjectEnvironment::new(
460 s.manifest_dir.join("benches/complex_benches"),
461 "complex-benches",
462 );
463 assert!(!s.is_crate_root());
464 }
465
466 #[test]
467 fn test_is_crate_name_in_dirs() {
468 let (s, _dir) = test_project();
469 let root = &s.manifest_dir;
470
471 for dir_name in DIRS {
472 for ty in ["simple", "complex"] {
473 let s = ProjectEnvironment::new(root, format!("{ty}_{dir_name}"));
474 assert!(s.is_crate_name_in_dirs(), "{s:?}");
475 }
476 }
477
478 let s = ProjectEnvironment::new(root, "non_existant");
479 assert!(!s.is_crate_name_in_dirs());
480 let s = ProjectEnvironment::new(root.join("does-not-exist"), "foo_bar");
481 assert!(!s.is_crate_name_in_dirs());
482 }
483
484 #[test]
485 fn test_file_stem_eq() {
486 let path = Path::new("/tmp/foo.rs");
487 assert!(file_stem_eq(path, "foo"));
488 assert!(!file_stem_eq(path, "tmp"));
489 assert!(!file_stem_eq(path, "foo.rs"));
490 assert!(!file_stem_eq(path, "fo"));
491 assert!(!file_stem_eq(path, "f"));
492 assert!(!file_stem_eq(path, ""));
493
494 let path = Path::new("/tmp/foo/");
495 assert!(file_stem_eq(path, "foo"));
496 assert!(!file_stem_eq(path, "tmp"));
497 assert!(!file_stem_eq(path, "fo"));
498 assert!(!file_stem_eq(path, "f"));
499 assert!(!file_stem_eq(path, ""));
500 }
501
502 fn test_project() -> (ProjectEnvironment, TempDir) {
518 let dir = tempfile::Builder::new().prefix("tmp").tempdir().unwrap();
520 let root = dir.path();
521 let name = root.file_name().unwrap().to_str().unwrap();
522
523 fs::create_dir_all(root).unwrap();
525 let src = root.join("src");
526 fs::create_dir(&src).unwrap();
527 fs::write(src.join("main.rs"), "fn main(){}").unwrap();
528
529 for dir_name in DIRS {
530 let new_dir = root.join(dir_name);
531 fs::create_dir(&new_dir).unwrap();
532
533 let simple = new_dir.join(format!("simple_{dir_name}.rs"));
534 fs::write(simple, "").unwrap();
535
536 let mut complex = new_dir.join(format!("complex_{dir_name}"));
537 if dir_name != "tests" {
538 fs::create_dir(&complex).unwrap();
539 fs::write(complex.join("Cargo.toml"), "").unwrap();
540 complex.push("src");
541 }
542 fs::create_dir(&complex).unwrap();
543 fs::write(complex.join("main.rs"), "").unwrap();
544 fs::write(complex.join("module.rs"), "").unwrap();
545 }
546
547 let target = root.join("target");
549 fs::create_dir(&target).unwrap();
550 fs::create_dir_all(target.join("tmp")).unwrap();
551
552 (ProjectEnvironment::new(root, name), dir)
553 }
554
555 fn write_manifest(s: &ProjectEnvironment, ethers: bool, dependencies: &[EthersCrate]) {
557 const ETHERS_CORE: &str = env!("CARGO_MANIFEST_DIR");
559 let ethers_root = Path::new(ETHERS_CORE).parent().unwrap();
560 let mut dependencies_toml =
561 String::with_capacity(150 * (ethers as usize + dependencies.len()));
562
563 if ethers {
564 let path = ethers_root.join("ethers");
565 let ethers = format!("ethers = {{ path = \"{}\" }}\n", path.display());
566 dependencies_toml.push_str(ðers);
567 }
568
569 for dep in dependencies.iter() {
570 let path = ethers_root.join(dep.fs_path());
571 let dep = format!("{dep} = {{ path = \"{}\" }}\n", path.display());
572 dependencies_toml.push_str(&dep);
573 }
574
575 let contents = format!(
576 r#"
577[package]
578name = "{}"
579version = "0.0.0"
580edition = "2021"
581
582[dependencies]
583{dependencies_toml}
584"#,
585 s.crate_name.as_ref().unwrap()
586 );
587 fs::write(s.manifest_dir.join("Cargo.toml"), contents).unwrap();
588 }
589}