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
///