From 11968eae7a9ec37191860c31ac608d1bc75d2dd0 Mon Sep 17 00:00:00 2001 From: 0129general Date: Wed, 19 Jan 2022 09:58:52 -0800 Subject: [PATCH] feat(forge): flatten (#506) * add flatten support * remove linked libs opt * address PR comments * chore: bump ethers to fix flatten bug https://github.com/gakonst/ethers-rs/pull/813 Co-authored-by: Georgios Konstantopoulos --- Cargo.lock | 22 ++++----- cli/src/cmd/build.rs | 59 ++++------------------- cli/src/cmd/flatten.rs | 106 +++++++++++++++++++++++++++++++++++++++++ cli/src/cmd/mod.rs | 1 + cli/src/forge.rs | 3 ++ cli/src/opts/forge.rs | 6 ++- cli/src/utils.rs | 62 +++++++++++++++++++++++- 7 files changed, 194 insertions(+), 65 deletions(-) create mode 100644 cli/src/cmd/flatten.rs diff --git a/Cargo.lock b/Cargo.lock index a31efe1..29b2972 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1194,7 +1194,7 @@ dependencies = [ [[package]] name = "ethers" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#eb555c28cca0dbd6a6cafda96fb96d7c4a903abf" +source = "git+https://github.com/gakonst/ethers-rs#3893f2f9b069b818f0e1588145313d332ac9f928" dependencies = [ "ethers-addressbook", "ethers-contract", @@ -1209,7 +1209,7 @@ dependencies = [ [[package]] name = "ethers-addressbook" version = "0.1.0" -source = "git+https://github.com/gakonst/ethers-rs#eb555c28cca0dbd6a6cafda96fb96d7c4a903abf" +source = "git+https://github.com/gakonst/ethers-rs#3893f2f9b069b818f0e1588145313d332ac9f928" dependencies = [ "ethers-core", "once_cell", @@ -1220,7 +1220,7 @@ dependencies = [ [[package]] name = "ethers-contract" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#eb555c28cca0dbd6a6cafda96fb96d7c4a903abf" +source = "git+https://github.com/gakonst/ethers-rs#3893f2f9b069b818f0e1588145313d332ac9f928" dependencies = [ "ethers-contract-abigen", "ethers-contract-derive", @@ -1238,7 +1238,7 @@ dependencies = [ [[package]] name = "ethers-contract-abigen" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#eb555c28cca0dbd6a6cafda96fb96d7c4a903abf" +source = "git+https://github.com/gakonst/ethers-rs#3893f2f9b069b818f0e1588145313d332ac9f928" dependencies = [ "Inflector", "anyhow", @@ -1261,7 +1261,7 @@ dependencies = [ [[package]] name = "ethers-contract-derive" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#eb555c28cca0dbd6a6cafda96fb96d7c4a903abf" +source = "git+https://github.com/gakonst/ethers-rs#3893f2f9b069b818f0e1588145313d332ac9f928" dependencies = [ "ethers-contract-abigen", "ethers-core", @@ -1275,7 +1275,7 @@ dependencies = [ [[package]] name = "ethers-core" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#eb555c28cca0dbd6a6cafda96fb96d7c4a903abf" +source = "git+https://github.com/gakonst/ethers-rs#3893f2f9b069b818f0e1588145313d332ac9f928" dependencies = [ "arrayvec 0.7.2", "bytes", @@ -1303,7 +1303,7 @@ dependencies = [ [[package]] name = "ethers-etherscan" version = "0.2.0" -source = "git+https://github.com/gakonst/ethers-rs#eb555c28cca0dbd6a6cafda96fb96d7c4a903abf" +source = "git+https://github.com/gakonst/ethers-rs#3893f2f9b069b818f0e1588145313d332ac9f928" dependencies = [ "ethers-core", "reqwest", @@ -1316,7 +1316,7 @@ dependencies = [ [[package]] name = "ethers-middleware" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#eb555c28cca0dbd6a6cafda96fb96d7c4a903abf" +source = "git+https://github.com/gakonst/ethers-rs#3893f2f9b069b818f0e1588145313d332ac9f928" dependencies = [ "async-trait", "ethers-contract", @@ -1339,7 +1339,7 @@ dependencies = [ [[package]] name = "ethers-providers" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#eb555c28cca0dbd6a6cafda96fb96d7c4a903abf" +source = "git+https://github.com/gakonst/ethers-rs#3893f2f9b069b818f0e1588145313d332ac9f928" dependencies = [ "async-trait", "auto_impl", @@ -1369,7 +1369,7 @@ dependencies = [ [[package]] name = "ethers-signers" version = "0.6.0" -source = "git+https://github.com/gakonst/ethers-rs#eb555c28cca0dbd6a6cafda96fb96d7c4a903abf" +source = "git+https://github.com/gakonst/ethers-rs#3893f2f9b069b818f0e1588145313d332ac9f928" dependencies = [ "async-trait", "coins-bip32", @@ -1392,7 +1392,7 @@ dependencies = [ [[package]] name = "ethers-solc" version = "0.1.0" -source = "git+https://github.com/gakonst/ethers-rs#eb555c28cca0dbd6a6cafda96fb96d7c4a903abf" +source = "git+https://github.com/gakonst/ethers-rs#3893f2f9b069b818f0e1588145313d332ac9f928" dependencies = [ "colored", "dunce", diff --git a/cli/src/cmd/build.rs b/cli/src/cmd/build.rs index fd9edfd..c82877f 100644 --- a/cli/src/cmd/build.rs +++ b/cli/src/cmd/build.rs @@ -2,13 +2,11 @@ use ethers::solc::{ artifacts::{Optimizer, Settings}, - remappings::Remapping, MinimalCombinedArtifacts, Project, ProjectCompileOutput, ProjectPathsConfig, SolcConfig, }; use std::{ collections::BTreeMap, path::{Path, PathBuf}, - str::FromStr, }; use crate::{cmd::Cmd, opts::forge::CompilerArgs, utils}; @@ -118,26 +116,6 @@ impl BuildArgs { } } - /// Determines the libraries - fn libs(&self, root: impl AsRef) -> Vec { - let root = root.as_ref(); - if self.lib_paths.is_empty() { - if self.hardhat { - vec![root.join("node_modules")] - } else { - // no libs directories provided - ProjectPathsConfig::find_libs(&root) - } - } else { - let mut libs = self.lib_paths.clone(); - if self.hardhat && !self.lib_paths.iter().any(|lib| lib.ends_with("node_modules")) { - // if --hardhat was set, ensure it is present in the lib set - libs.push(root.join("node_modules")); - } - libs - } - } - /// Converts all build arguments to the corresponding project config /// /// Defaults to DAppTools-style repo layout, but can be customized. @@ -156,35 +134,14 @@ impl BuildArgs { // 4. Set where the libraries are going to be read from // default to the lib path being the `lib/` dir - let lib_paths = self.libs(&root); - - // get all the remappings corresponding to the lib paths - let mut remappings: Vec<_> = lib_paths.iter().flat_map(Remapping::find_many).collect(); - - // extend them with the once manually provided in the opts - remappings.extend_from_slice(&self.remappings); - - // extend them with the one via the env vars - if let Some(ref env) = self.remappings_env { - remappings.extend(remappings_from_newline(env)) - } - - // extend them with the one via the requirements.txt - if let Ok(ref remap) = std::fs::read_to_string(root.join("remappings.txt")) { - remappings.extend(remappings_from_newline(remap)) - } - - // helper function for parsing newline-separated remappings - fn remappings_from_newline(remappings: &str) -> impl Iterator + '_ { - remappings.split('\n').filter(|x| !x.is_empty()).map(|x| { - Remapping::from_str(x) - .unwrap_or_else(|_| panic!("could not parse remapping: {}", x)) - }) - } - - // remove any potential duplicates - remappings.sort_unstable(); - remappings.dedup(); + let lib_paths = utils::find_libs(&root, &self.lib_paths, self.hardhat); + + let remappings = utils::find_remappings( + &lib_paths, + &self.remappings, + &root.join("remappings.txt"), + &self.remappings_env, + ); // build the path let mut paths_builder = diff --git a/cli/src/cmd/flatten.rs b/cli/src/cmd/flatten.rs new file mode 100644 index 0000000..9736f5e --- /dev/null +++ b/cli/src/cmd/flatten.rs @@ -0,0 +1,106 @@ +use std::path::PathBuf; + +use ethers::solc::{remappings::Remapping, ProjectPathsConfig}; + +use crate::{cmd::Cmd, utils}; +use clap::{Parser, ValueHint}; + +#[derive(Debug, Clone, Parser)] +pub struct FlattenArgs { + #[clap(help = "the path to the contract to flatten", value_hint = ValueHint::FilePath)] + pub target_path: PathBuf, + + #[clap(long, short, help = "output path for the flattened contract", value_hint = ValueHint::FilePath)] + pub output: Option, + + #[clap( + help = "the project's root path. By default, this is the root directory of the current Git repository or the current working directory if it is not part of a Git repository", + long, + value_hint = ValueHint::DirPath + )] + pub root: Option, + + #[clap( + env = "DAPP_SRC", + help = "the directory relative to the root under which the smart contracts are", + long, + short, + value_hint = ValueHint::DirPath + )] + pub contracts: Option, + + #[clap(help = "the remappings", long, short)] + pub remappings: Vec, + #[clap(long = "remappings-env", env = "DAPP_REMAPPINGS")] + pub remappings_env: Option, + + #[clap( + help = "the paths where your libraries are installed", + long, + value_hint = ValueHint::DirPath + )] + pub lib_paths: Vec, + + #[clap( + help = "uses hardhat style project layout. This a convenience flag and is the same as `--contracts contracts --lib-paths node_modules`", + long, + conflicts_with = "contracts", + alias = "hh" + )] + pub hardhat: bool, +} + +impl Cmd for FlattenArgs { + type Output = (); + fn run(self) -> eyre::Result { + let root = self.root.clone().unwrap_or_else(|| { + utils::find_git_root_path().unwrap_or_else(|_| std::env::current_dir().unwrap()) + }); + let root = dunce::canonicalize(&root)?; + + let contracts = match self.contracts { + Some(ref contracts) => root.join(contracts), + None => { + if self.hardhat { + root.join("contracts") + } else { + // no contract source directory was provided, determine the source directory + ProjectPathsConfig::find_source_dir(&root) + } + } + }; + + let lib_paths = utils::find_libs(&root, &self.lib_paths, self.hardhat); + + let remappings = utils::find_remappings( + &lib_paths, + &self.remappings, + &root.join("remappings.txt"), + &self.remappings_env, + ); + + // build the path + let mut paths_builder = ProjectPathsConfig::builder().root(&root).sources(contracts); + + if !remappings.is_empty() { + paths_builder = paths_builder.remappings(remappings); + } + + let paths = paths_builder.build()?; + let target_path = dunce::canonicalize(self.target_path)?; + let flattened = paths + .flatten(&target_path) + .map_err(|err| eyre::Error::msg(format!("failed to flatten the file: {}", err)))?; + + match self.output { + Some(output) => { + std::fs::create_dir_all(&output.parent().unwrap())?; + std::fs::write(&output, flattened)?; + println!("Flattened file written at {}", output.display()); + } + None => println!("{}", flattened), + }; + + Ok(()) + } +} diff --git a/cli/src/cmd/mod.rs b/cli/src/cmd/mod.rs index 168f88f..d19ecd4 100644 --- a/cli/src/cmd/mod.rs +++ b/cli/src/cmd/mod.rs @@ -2,6 +2,7 @@ pub mod build; pub mod create; +pub mod flatten; pub mod remappings; pub mod run; pub mod snapshot; diff --git a/cli/src/forge.rs b/cli/src/forge.rs index 96a0bc1..f761c12 100644 --- a/cli/src/forge.rs +++ b/cli/src/forge.rs @@ -128,6 +128,9 @@ fn main() -> eyre::Result<()> { Subcommands::Snapshot(cmd) => { cmd.run()?; } + Subcommands::Flatten(cmd) => { + cmd.run()?; + } } Ok(()) diff --git a/cli/src/opts/forge.rs b/cli/src/opts/forge.rs index 19e0d72..f44ec2e 100644 --- a/cli/src/opts/forge.rs +++ b/cli/src/opts/forge.rs @@ -4,7 +4,8 @@ use ethers::{solc::EvmVersion, types::Address}; use std::{path::PathBuf, str::FromStr}; use crate::cmd::{ - build::BuildArgs, create::CreateArgs, remappings::RemappingArgs, run::RunArgs, snapshot, test, + build::BuildArgs, create::CreateArgs, flatten, remappings::RemappingArgs, run::RunArgs, + snapshot, test, }; #[derive(Debug, Parser)] @@ -98,6 +99,9 @@ pub enum Subcommands { #[clap(about = "creates a snapshot of each test's gas usage")] Snapshot(snapshot::SnapshotArgs), + + #[clap(about = "concats a file with all of its imports")] + Flatten(flatten::FlattenArgs), } #[derive(Debug, Clone, Parser)] diff --git a/cli/src/utils.rs b/cli/src/utils.rs index 9428a91..e023f45 100644 --- a/cli/src/utils.rs +++ b/cli/src/utils.rs @@ -1,7 +1,11 @@ -use ethers::solc::{artifacts::Contract, EvmVersion}; +use ethers::solc::{artifacts::Contract, remappings::Remapping, EvmVersion, ProjectPathsConfig}; use eyre::{ContextCompat, WrapErr}; -use std::{path::PathBuf, process::Command}; +use std::{ + path::{Path, PathBuf}, + process::Command, + str::FromStr, +}; #[cfg(feature = "evmodin-evm")] use evmodin::Revision; @@ -98,3 +102,57 @@ pub fn read_secret(secret: bool, unsafe_secret: Option) -> eyre::Result< unsafe_secret.unwrap() }) } + +/// Find and parse out all the remappings for the projects +pub fn find_remappings( + libs: &[PathBuf], + remappings: &[Remapping], + remappings_txt: &Path, + remappings_env: &Option, +) -> Vec { + /// Helper function for parsing newline-separated remappings + fn remappings_from_newline(remappings: &str) -> impl Iterator + '_ { + remappings.lines().filter(|x| !x.trim().is_empty()).map(|x| { + Remapping::from_str(x).unwrap_or_else(|_| panic!("could not parse remapping: {}", x)) + }) + } + + let mut result: Vec<_> = libs.iter().flat_map(Remapping::find_many).collect(); + + result.extend_from_slice(remappings); + + // extend them with the one via the env vars + if let Some(ref env) = remappings_env { + result.extend(remappings_from_newline(env)) + } + + // extend them with the one via the requirements.txt + if let Ok(ref remap) = std::fs::read_to_string(remappings_txt) { + result.extend(remappings_from_newline(remap)) + } + + // remove any potential duplicates + result.sort_unstable(); + result.dedup(); + + result +} + +/// Find libraries for the project +pub fn find_libs(root: &Path, lib_paths: &[PathBuf], hardhat: bool) -> Vec { + if lib_paths.is_empty() { + if hardhat { + return vec![root.join("node_modules")] + } + + // no libs directories provided + return ProjectPathsConfig::find_libs(&root) + } + + let mut libs = lib_paths.to_vec(); + if hardhat && !lib_paths.iter().any(|lib| lib.ends_with("node_modules")) { + // if --hardhat was set, ensure it is present in the lib set + libs.push(root.join("node_modules")); + } + libs +}