Skip to main content

cbrzn_ethers_core/macros/
ethers_crate.rs

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
10/// `ethers_crate => name`
11type CrateNames = HashMap<EthersCrate, &'static str>;
12
13const DIRS: [&str; 3] = ["benches", "examples", "tests"];
14
15/// Maps an [`EthersCrate`] to its path string.
16///
17/// See [`ProjectEnvironment`] for more information.
18///
19/// Note: this static variable cannot hold [`syn::Path`] because it is not [`Sync`], so the names
20/// must be parsed at every call.
21static 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/// Returns the `core` crate's [`Path`][syn::Path].
28#[inline]
29pub fn ethers_core_crate() -> syn::Path {
30    get_crate_path(EthersCrate::EthersCore)
31}
32
33/// Returns the `contract` crate's [`Path`][syn::Path].
34#[inline]
35pub fn ethers_contract_crate() -> syn::Path {
36    get_crate_path(EthersCrate::EthersContract)
37}
38
39/// Returns the `providers` crate's [`Path`][syn::Path].
40#[inline]
41pub fn ethers_providers_crate() -> syn::Path {
42    get_crate_path(EthersCrate::EthersProviders)
43}
44
45/// Returns an [`EthersCrate`]'s [`Path`][syn::Path] in the current project.
46#[inline(always)]
47pub fn get_crate_path(krate: EthersCrate) -> syn::Path {
48    krate.get_path()
49}
50
51/// Represents a generic Rust/Cargo project's environment.
52#[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    /// Determines the crate paths to use by looking at the [metadata][cargo_metadata] of the
71    /// project.
72    ///
73    /// The names will be:
74    /// - `ethers::*` if `ethers` is a dependency for all crates;
75    /// - for each `crate`:
76    ///   - `ethers_<crate>` if it is a dependency, otherwise `ethers::<crate>`.
77    #[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        // remove the lock file created from running the command
85        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        // return ethers_* if the root package is an internal ethers crate since `ethers` is not
98        // available
99        let crate_is_root = self.is_crate_root();
100        if let Ok(current_pkg) = pkg.name.parse::<EthersCrate>() {
101            // replace `current_pkg`'s name with "crate"
102            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        } /* else if pkg.name == "ethers" {
114              // should not happen (the root package the `ethers` workspace package itself)
115          } */
116
117        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    /// Returns whether the `crate` path identifier refers to the root package.
132    ///
133    /// This is false for integration tests, benches, and examples, as the `crate` keyword will not
134    /// refer to the root package.
135    ///
136    /// We can find this using some [environment variables set by Cargo during compilation][ref]:
137    /// - `CARGO_TARGET_TMPDIR` is only set when building integration test or benchmark code;
138    /// - When `CARGO_MANIFEST_DIR` contains `/benches/` or `/examples/`
139    /// - `CARGO_CRATE_NAME`, see `is_crate_name_in_dirs`.
140    ///
141    /// [ref]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates
142    #[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    /// Returns whether `crate_name` is the name of a file or directory in the first level of
153    /// `manifest_dir/{benches,examples,tests}/`.
154    ///
155    /// # Example
156    ///
157    /// With this project structure:
158    ///
159    /// ```text
160    /// .
161    /// ├── Cargo.lock
162    /// ├── Cargo.toml
163    /// ├── src/
164    /// │   ...
165    /// ├── benches/
166    /// │   ├── large-input.rs
167    /// │   └── multi-file-bench/
168    /// │       ├── main.rs
169    /// │       └── bench_module.rs
170    /// ├── examples/
171    /// │   ├── simple.rs
172    /// │   └── multi-file-example/
173    /// │       ├── main.rs
174    /// │       └── ex_module.rs
175    /// └── tests/
176    ///     ├── some-integration-tests.rs
177    ///     └── multi-file-test/
178    ///         ├── main.rs
179    ///         └── test_module.rs
180    /// ```
181    ///
182    /// The resulting `CARGO_CRATE_NAME` values will be:
183    ///
184    /// |                  Path                  |          Value         |
185    /// |:-------------------------------------- | ----------------------:|
186    /// | benches/large-input.rs                 |            large-input |
187    /// | benches/multi-file-bench/\*\*/\*.rs    |       multi-file-bench |
188    /// | examples/simple.rs                     |                 simple |
189    /// | examples/multi-file-example/\*\*/\*.rs |     multi-file-example |
190    /// | tests/some-integration-tests.rs        | some-integration-tests |
191    /// | tests/multi-file-test/\*\*/\*.rs       |        multi-file-test |
192    #[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/// An `ethers-rs` internal crate.
213#[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    /// "`<self as kebab-case>`"
256    #[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    /// "`::<self as snake_case>`"
274    #[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    /// "::ethers::`<self in ethers>`"
292    #[inline]
293    pub const fn ethers_path_name(self) -> &'static str {
294        match self {
295            // re-exported in ethers::contract
296            Self::EthersContractAbigen => "::ethers::contract", // partly
297            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    /// The path on the file system, from an `ethers-rs` root directory.
312    #[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    /// `<ethers_*>`
323    #[inline]
324    pub fn path_names() -> impl Iterator<Item = (Self, &'static str)> {
325        Self::iter().map(|x| (x, x.path_name()))
326    }
327
328    /// `<ethers::*>`
329    #[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    /// Returns the [`Path`][syn::Path] in the current project.
335    #[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/// `path.file_stem() == s`
343#[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            // SAFETY: generates in the safe range
370            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            // speeds up consecutive runs by not having to re-create and delete the lockfile
380            // this is tested separately: test_lock_file
381            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                // internal
391                (true, _) => EthersCrate::path_names().collect(),
392
393                // ethers
394                (_, true) => EthersCrate::ethers_path_names().collect(),
395
396                // no ethers
397                (_, 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            // don't use assert for a better custom message
411            if names != expected {
412                // BTreeMap sorts the keys
413                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        // crate_name        -> represents an external crate
432        // "ethers-contract" -> represents an internal crate
433        for name in [s.crate_name.as_ref().unwrap(), "ethers-contract"] {
434            let s = ProjectEnvironment::new(&s.manifest_dir, name);
435            // only ethers
436            assert_names(&s, true, &[]);
437
438            // only others
439            assert_names(&s, false, gen_unique::<3>().as_slice());
440
441            // ethers and others
442            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        // `CARGO_MANIFEST_DIR`
470        // complex path has `/{dir_name}/` in the path
471        // name or path validity not checked
472        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    // utils
521
522    /// Creates:
523    ///
524    /// ```text
525    /// - new_dir
526    ///   - src
527    ///     - main.rs
528    ///   - {dir_name} for dir_name in DIRS
529    ///     - simple_{dir_name}.rs
530    ///     - complex_{dir_name}
531    ///       - src if not "tests"
532    ///         - main.rs
533    ///         - module.rs
534    /// ```
535    fn test_project() -> (ProjectEnvironment, TempDir) {
536        // change the prefix to one without the default `.` because it is not a valid crate name
537        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        // No Cargo.toml, git
542        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        // create target dirs
566        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    /// Writes a test manifest to `{root}/Cargo.toml`.
574    fn write_manifest(s: &ProjectEnvironment, ethers: bool, dependencies: &[EthersCrate]) {
575        // use paths to avoid downloading dependencies
576        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(&ethers);
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}