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
This commit is contained in:
Matthias Seitz 2021-11-29 14:37:11 +01:00 committed by GitHub
parent d06bfdc15c
commit f6e803b4eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 94 additions and 27 deletions

View File

@ -2,14 +2,10 @@
use super::util; use super::util;
use ethers_core::types::Address; use ethers_core::types::Address;
use crate::util::resolve_path;
use anyhow::{anyhow, Context, Error, Result}; use anyhow::{anyhow, Context, Error, Result};
use cfg_if::cfg_if; use cfg_if::cfg_if;
use std::{ use std::{env, fs, path::Path, str::FromStr};
borrow::Cow,
env, fs,
path::{Path, PathBuf},
str::FromStr,
};
use url::Url; use url::Url;
/// A source of a Truffle artifact JSON. /// A source of a Truffle artifact JSON.
@ -19,7 +15,7 @@ pub enum Source {
String(String), String(String),
/// An ABI located on the local file system. /// An ABI located on the local file system.
Local(PathBuf), Local(String),
/// An ABI to be retrieved over HTTP(S). /// An ABI to be retrieved over HTTP(S).
Http(Url), Http(Url),
@ -94,7 +90,7 @@ impl Source {
let url = base.join(source.as_ref())?; let url = base.join(source.as_ref())?;
match url.scheme() { match url.scheme() {
"file" => Ok(Source::local(url.path())), "file" => Ok(Source::local(url.path().to_string())),
"http" | "https" => match url.host_str() { "http" | "https" => match url.host_str() {
Some("etherscan.io") => Source::etherscan( Some("etherscan.io") => Source::etherscan(
url.path() url.path()
@ -111,11 +107,8 @@ impl Source {
} }
/// Creates a local filesystem source from a path string. /// Creates a local filesystem source from a path string.
pub fn local<P>(path: P) -> Self pub fn local(path: impl Into<String>) -> Self {
where Source::Local(path.into())
P: AsRef<Path>,
{
Source::Local(path.as_ref().into())
} }
/// Creates an HTTP source from a URL. /// 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. /// Reads an artifact JSON file from the local filesystem.
fn get_local_contract(path: &Path) -> Result<String> { ///
/// 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<str>) -> Result<String> {
let path = resolve_path(path.as_ref())?;
let path = if path.is_relative() { let path = if path.is_relative() {
let absolute_path = path.canonicalize().with_context(|| { let manifest_path = env::var("CARGO_MANIFEST_DIR")?;
format!( let root = Path::new(&manifest_path);
"unable to canonicalize file from working dir {} with path {}", let mut contract_path = root.join(&path);
env::current_dir() if !contract_path.exists() {
.map(|cwd| cwd.display().to_string()) contract_path = path.canonicalize()?;
.unwrap_or_else(|err| format!("??? ({})", err)), }
path.display(), if !contract_path.exists() {
) anyhow::bail!("Unable to find local contract \"{}\"", path.display())
})?; }
Cow::Owned(absolute_path) contract_path
} else { } else {
Cow::Borrowed(path) path
}; };
let json = fs::read_to_string(&path) let json = fs::read_to_string(&path)

View File

@ -1,4 +1,5 @@
use ethers_core::types::Address; use ethers_core::types::Address;
use std::path::PathBuf;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use cfg_if::cfg_if; use cfg_if::cfg_if;
@ -81,10 +82,75 @@ pub fn http_get(_url: &str) -> Result<String> {
} }
} }
/// Replaces any occurrences of env vars in the `raw` str with their value
pub fn resolve_path(raw: &str) -> Result<PathBuf> {
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)] #[cfg(test)]
mod tests { mod tests {
use super::*; 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] #[test]
fn input_name_to_ident_empty() { fn input_name_to_ident_empty() {
assert_quote!(expand_input_name(0, ""), { p0 }); assert_quote!(expand_input_name(0, ""), { p0 });

View File

@ -16,8 +16,10 @@ mod spanned;
pub(crate) mod utils; pub(crate) mod utils;
/// Proc macro to generate type-safe bindings to a contract(s). This macro /// 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`. /// rooted in the crate's root `CARGO_MANIFEST_DIR`.
/// Environment variable interpolation is supported via `$` prefix, like
/// `"$CARGO_MANIFEST_DIR/contracts/c.json"`
/// ///
/// # Examples /// # Examples
/// ///