From f6e803b4ebe839a1d97788f7601c35f039cc358e Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Mon, 29 Nov 2021 14:37:11 +0100 Subject: [PATCH] fix: use CARGO_MANIFEST_DIR as root for relative paths in abigen! (#631) * feat: resolve env vars in abigen paths * docs: add env interpolation note * chore: docs --- .../ethers-contract-abigen/src/source.rs | 51 +++++++------- .../ethers-contract-abigen/src/util.rs | 66 +++++++++++++++++++ .../ethers-contract-derive/src/lib.rs | 4 +- 3 files changed, 94 insertions(+), 27 deletions(-) diff --git a/ethers-contract/ethers-contract-abigen/src/source.rs b/ethers-contract/ethers-contract-abigen/src/source.rs index 10695583..ec464faa 100644 --- a/ethers-contract/ethers-contract-abigen/src/source.rs +++ b/ethers-contract/ethers-contract-abigen/src/source.rs @@ -2,14 +2,10 @@ use super::util; use ethers_core::types::Address; +use crate::util::resolve_path; use anyhow::{anyhow, Context, Error, Result}; use cfg_if::cfg_if; -use std::{ - borrow::Cow, - env, fs, - path::{Path, PathBuf}, - str::FromStr, -}; +use std::{env, fs, path::Path, str::FromStr}; use url::Url; /// A source of a Truffle artifact JSON. @@ -19,7 +15,7 @@ pub enum Source { String(String), /// An ABI located on the local file system. - Local(PathBuf), + Local(String), /// An ABI to be retrieved over HTTP(S). Http(Url), @@ -94,7 +90,7 @@ impl Source { let url = base.join(source.as_ref())?; match url.scheme() { - "file" => Ok(Source::local(url.path())), + "file" => Ok(Source::local(url.path().to_string())), "http" | "https" => match url.host_str() { Some("etherscan.io") => Source::etherscan( url.path() @@ -111,11 +107,8 @@ impl Source { } /// Creates a local filesystem source from a path string. - pub fn local

(path: P) -> Self - where - P: AsRef, - { - Source::Local(path.as_ref().into()) + pub fn local(path: impl Into) -> Self { + Source::Local(path.into()) } /// Creates an HTTP source from a URL. @@ -178,21 +171,27 @@ impl FromStr for Source { } } -/// Reads a Truffle artifact JSON file from the local filesystem. -fn get_local_contract(path: &Path) -> Result { +/// Reads an artifact JSON file from the local filesystem. +/// +/// The given path can be relative or absolute and can contain env vars like +/// `"$CARGO_MANIFEST_DIR/contracts/a.json"` +/// If the path is relative after all env vars have been resolved then we assume the root is either +/// `CARGO_MANIFEST_DIR` or the current working directory. +fn get_local_contract(path: impl AsRef) -> Result { + let path = resolve_path(path.as_ref())?; let path = if path.is_relative() { - let absolute_path = path.canonicalize().with_context(|| { - format!( - "unable to canonicalize file from working dir {} with path {}", - env::current_dir() - .map(|cwd| cwd.display().to_string()) - .unwrap_or_else(|err| format!("??? ({})", err)), - path.display(), - ) - })?; - Cow::Owned(absolute_path) + let manifest_path = env::var("CARGO_MANIFEST_DIR")?; + let root = Path::new(&manifest_path); + let mut contract_path = root.join(&path); + if !contract_path.exists() { + contract_path = path.canonicalize()?; + } + if !contract_path.exists() { + anyhow::bail!("Unable to find local contract \"{}\"", path.display()) + } + contract_path } else { - Cow::Borrowed(path) + path }; let json = fs::read_to_string(&path) diff --git a/ethers-contract/ethers-contract-abigen/src/util.rs b/ethers-contract/ethers-contract-abigen/src/util.rs index d7b5fc60..3252e047 100644 --- a/ethers-contract/ethers-contract-abigen/src/util.rs +++ b/ethers-contract/ethers-contract-abigen/src/util.rs @@ -1,4 +1,5 @@ use ethers_core::types::Address; +use std::path::PathBuf; use anyhow::{anyhow, Result}; use cfg_if::cfg_if; @@ -81,10 +82,75 @@ pub fn http_get(_url: &str) -> Result { } } +/// Replaces any occurrences of env vars in the `raw` str with their value +pub fn resolve_path(raw: &str) -> Result { + let mut unprocessed = raw; + let mut resolved = String::new(); + + while let Some(dollar_sign) = unprocessed.find('$') { + let (head, tail) = unprocessed.split_at(dollar_sign); + resolved.push_str(head); + + match parse_identifier(&tail[1..]) { + Some((variable, rest)) => { + let value = std::env::var(variable)?; + resolved.push_str(&value); + unprocessed = rest; + } + None => { + anyhow::bail!("Unable to parse a variable from \"{}\"", tail) + } + } + } + resolved.push_str(unprocessed); + + Ok(PathBuf::from(resolved)) +} + +fn parse_identifier(text: &str) -> Option<(&str, &str)> { + let mut calls = 0; + + let (head, tail) = take_while(text, |c| { + calls += 1; + match c { + '_' => true, + letter if letter.is_ascii_alphabetic() => true, + digit if digit.is_ascii_digit() && calls > 1 => true, + _ => false, + } + }); + + if head.is_empty() { + None + } else { + Some((head, tail)) + } +} + +fn take_while(s: &str, mut predicate: impl FnMut(char) -> bool) -> (&str, &str) { + let mut index = 0; + for c in s.chars() { + if predicate(c) { + index += c.len_utf8(); + } else { + break + } + } + s.split_at(index) +} + #[cfg(test)] mod tests { use super::*; + #[test] + fn can_resolve_path() { + let raw = "./$ENV_VAR"; + std::env::set_var("ENV_VAR", "file.txt"); + let resolved = resolve_path(raw).unwrap(); + assert_eq!(resolved.to_str().unwrap(), "./file.txt"); + } + #[test] fn input_name_to_ident_empty() { assert_quote!(expand_input_name(0, ""), { p0 }); diff --git a/ethers-contract/ethers-contract-derive/src/lib.rs b/ethers-contract/ethers-contract-derive/src/lib.rs index d343be2c..1ab7bec0 100644 --- a/ethers-contract/ethers-contract-derive/src/lib.rs +++ b/ethers-contract/ethers-contract-derive/src/lib.rs @@ -16,8 +16,10 @@ mod spanned; pub(crate) mod utils; /// Proc macro to generate type-safe bindings to a contract(s). This macro -/// accepts one or more Ethereum contract ABI or a path. Note that this path is +/// accepts one or more Ethereum contract ABI or a path. Note that relative paths are /// rooted in the crate's root `CARGO_MANIFEST_DIR`. +/// Environment variable interpolation is supported via `$` prefix, like +/// `"$CARGO_MANIFEST_DIR/contracts/c.json"` /// /// # Examples ///