diff --git a/ethers-core/src/macros/ethers_crate.rs b/ethers-core/src/macros/ethers_crate.rs index 78bc38d8..573775a0 100644 --- a/ethers-core/src/macros/ethers_crate.rs +++ b/ethers-core/src/macros/ethers_crate.rs @@ -1,123 +1,108 @@ +//! N.B.: +//! - crate names must not be [global paths](https://doc.rust-lang.org/reference/paths.html#path-qualifiers) +//! since we must be able to override them internally, like in Multicall. +//! +//! - [`ETHERS_CRATE_NAMES`] cannot hold [`syn::Path`] because it is not [`Sync`], so the names must +//! be parsed at every call. + use cargo_metadata::MetadataCommand; use once_cell::sync::Lazy; +use std::path::PathBuf; -use syn::Path; - -/// See `determine_ethers_crates` +/// Crate names to use in Path resolution. /// -/// This ensures that the `MetadataCommand` is only run once -static ETHERS_CRATES: Lazy<(&'static str, &'static str, &'static str)> = - Lazy::new(determine_ethers_crates); +/// `(core, contract, providers)` +type CrateNames = (&'static str, &'static str, &'static str); -/// Convenience function to turn the `ethers_core` name in `ETHERS_CRATE` into a `Path` -pub fn ethers_core_crate() -> Path { - syn::parse_str(ETHERS_CRATES.0).expect("valid path; qed") -} -/// Convenience function to turn the `ethers_contract` name in `ETHERS_CRATE` into an `Path` -pub fn ethers_contract_crate() -> Path { - syn::parse_str(ETHERS_CRATES.1).expect("valid path; qed") -} -pub fn ethers_providers_crate() -> Path { - syn::parse_str(ETHERS_CRATES.2).expect("valid path; qed") +const DEFAULT_CRATE_NAMES: CrateNames = ("ethers::core", "ethers::contract", "ethers::providers"); +const SUB_CRATE_NAMES: CrateNames = ("ethers_core", "ethers_contract", "ethers_providers"); + +/// See [`determine_ethers_crates`]. +/// +/// This ensures that the `MetadataCommand` is ran only once. +static ETHERS_CRATE_NAMES: Lazy = Lazy::new(determine_ethers_crates); + +/// Returns the `core` crate's [`Path`][syn::Path]. +pub fn ethers_core_crate() -> syn::Path { + syn::parse_str(ETHERS_CRATE_NAMES.0).unwrap() } -/// The crates name to use when deriving macros: (`core`, `contract`) +/// Returns the `contract` crate's [`Path`][syn::Path]. +pub fn ethers_contract_crate() -> syn::Path { + syn::parse_str(ETHERS_CRATE_NAMES.1).unwrap() +} + +/// Returns the `providers` crate's [`Path`][syn::Path]. +pub fn ethers_providers_crate() -> syn::Path { + syn::parse_str(ETHERS_CRATE_NAMES.2).unwrap() +} + +/// Determines which crate paths to use by looking at the [metadata][cargo_metadata] of the project. /// -/// We try to determine which crate ident to use based on the dependencies of -/// the project in which the macro is used. This is useful because the macros, -/// like `EthEvent` are provided by the `ethers-contract` crate which depends on -/// `ethers_core`. Most commonly `ethers` will be used as dependency which -/// reexports all the different crates, essentially `ethers::core` is -/// `ethers_core` So depending on the dependency used `ethers` ors `ethers_core -/// | ethers_contract`, we need to use the fitting crate ident when expand the -/// macros This will attempt to parse the current `Cargo.toml` and check the -/// ethers related dependencies. -/// -/// This determines -/// - `ethers_*` idents if `ethers-core`, `ethers-contract`, `ethers-providers` are present in -/// the manifest or the `ethers` is _not_ present -/// - `ethers::*` otherwise -/// -/// This process is a bit hacky, we run `cargo metadata` internally which -/// resolves the current package but creates a new `Cargo.lock` file in the -/// process. This is not a problem for regular workspaces but becomes an issue -/// during publishing with `cargo publish` if the project does not ignore -/// `Cargo.lock` in `.gitignore`, because then cargo can't proceed with -/// publishing the crate because the created `Cargo.lock` leads to a modified -/// workspace, not the `CARGO_MANIFEST_DIR` but the workspace `cargo publish` -/// created in `./target/package/..`. Therefore we check prior to executing -/// `cargo metadata` if a `Cargo.lock` file exists and delete it afterwards if -/// it was created by `cargo metadata`. -pub fn determine_ethers_crates() -> (&'static str, &'static str, &'static str) { - let manifest_dir = std::env::var("CARGO_MANIFEST_DIR"); +/// Returns `ethers_*` if *all* necessary dependencies are present, otherwise `ethers::*`. +fn determine_ethers_crates() -> CrateNames { + // always defined in Cargo projects + let manifest_dir: PathBuf = + std::env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is not defined").into(); - // if there is no cargo manifest, default to `ethers::`-style imports. - let manifest_dir = if let Ok(manifest_dir) = manifest_dir { - manifest_dir - } else { - return ("ethers::core", "ethers::contract", "ethers::providers") - }; + let lock_file = manifest_dir.join("Cargo.lock"); + let lock_file_existed = lock_file.exists(); - // check if the lock file exists, if it's missing we need to clean up afterward - let lock_file = format!("{manifest_dir}/Cargo.lock"); - let needs_lock_file_cleanup = !std::path::Path::new(&lock_file).exists(); + let names = crate_names_from_metadata(manifest_dir).unwrap_or(DEFAULT_CRATE_NAMES); - let res = MetadataCommand::new() - .manifest_path(&format!("{manifest_dir}/Cargo.toml")) - .exec() - .ok() - .and_then(|metadata| { - metadata.root_package().and_then(|pkg| { - let sub_crates = Some(("ethers_core", "ethers_contract", "ethers_providers")); - - // Note(mattsse): this is super hacky but required in order to compile and test - // ethers' internal crates - if [ - "ethers-contract", - "ethers-derive-eip712", - "ethers-signers", - "ethers-middleware", - "ethers-solc", - ] - .contains(&pkg.name.as_str()) - { - return sub_crates - } - - let mut has_ethers_core = false; - let mut has_ethers_contract = false; - let mut has_ethers_providers = false; - - for dep in pkg.dependencies.iter() { - match dep.name.as_str() { - "ethers-core" => { - has_ethers_core = true; - } - "ethers-contract" => { - has_ethers_contract = true; - } - "ethers-providers" => { - has_ethers_providers = true; - } - "ethers" => return None, - _ => {} - } - } - - if has_ethers_core && has_ethers_contract && has_ethers_providers { - return sub_crates - } - - None - }) - }) - .unwrap_or(("ethers::core", "ethers::contract", "ethers::providers")); - - if needs_lock_file_cleanup { - // delete the `Cargo.lock` file that was created by `cargo metadata` - // if the package is not part of a workspace + // remove the lock file created from running the command + if !lock_file_existed && lock_file.exists() { let _ = std::fs::remove_file(lock_file); } - res + names +} + +/// Runs [`cargo metadata`][MetadataCommand] from `manifest_dir` and determines the crate names to +/// use. +/// +/// Returns `None` on any error or if no dependencies are found. +#[inline] +fn crate_names_from_metadata(manifest_dir: PathBuf) -> Option { + let metadata = MetadataCommand::new().current_dir(manifest_dir).exec().ok()?; + let pkg = metadata.root_package()?; + + // HACK(mattsse): this is required in order to compile and test ethers' internal crates + const INTERNAL_CRATES: [&str; 5] = [ + "ethers-contract", + "ethers-derive-eip712", + "ethers-signers", + "ethers-middleware", + "ethers-solc", + ]; + let pkg_name = pkg.name.as_str(); + if INTERNAL_CRATES.contains(&pkg_name) { + return Some(SUB_CRATE_NAMES) + } + + let mut has_ethers_core = false; + let mut has_ethers_contract = false; + let mut has_ethers_providers = false; + + for dep in pkg.dependencies.iter() { + match dep.name.as_str() { + "ethers-core" => { + has_ethers_core = true; + } + "ethers-contract" => { + has_ethers_contract = true; + } + "ethers-providers" => { + has_ethers_providers = true; + } + "ethers" => return None, + _ => {} + } + } + + if has_ethers_core && has_ethers_contract && has_ethers_providers { + Some(SUB_CRATE_NAMES) + } else { + None + } }