From adb6abae69c7a0d766db123f66686cc890c22dd0 Mon Sep 17 00:00:00 2001 From: jpgonzalezra Date: Wed, 16 Oct 2024 09:51:13 -0300 Subject: [PATCH] feat(forge): add `compiler` subcommand (#7909) * feat(forge): add solc subcommand and utilities * style: improve formatting in solc.rs file * fix: merge * add json compatible output * add basic tests * add basic tests * clean up * finish tests * add skip flag * add vyper for unit tests * move tests, pin compiler version, use forgetest! * update CI test location for target Python / Vyper * update foundry-compilers crate * Update crates/forge/bin/cmd/compiler.rs Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> * `compiler` command is sync, remove conditions on CI for Vyper / Python installs * is_jsonlines -> is_json --------- Co-authored-by: zerosnacks Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> --- .github/workflows/nextest.yml | 2 - Cargo.lock | 20 +-- Cargo.toml | 2 +- crates/forge/bin/cmd/compiler.rs | 145 +++++++++++++++++++ crates/forge/bin/cmd/mod.rs | 1 + crates/forge/bin/main.rs | 1 + crates/forge/bin/opts.rs | 14 +- crates/forge/tests/cli/compiler.rs | 218 +++++++++++++++++++++++++++++ crates/forge/tests/cli/main.rs | 1 + 9 files changed, 386 insertions(+), 18 deletions(-) create mode 100644 crates/forge/bin/cmd/compiler.rs create mode 100644 crates/forge/tests/cli/compiler.rs diff --git a/.github/workflows/nextest.yml b/.github/workflows/nextest.yml index 8803540806de..da862c657abf 100644 --- a/.github/workflows/nextest.yml +++ b/.github/workflows/nextest.yml @@ -67,12 +67,10 @@ jobs: with: bun-version: latest - name: Setup Python - if: contains(matrix.name, 'external') uses: actions/setup-python@v4 with: python-version: 3.11 - name: Install Vyper - if: contains(matrix.name, 'external') run: pip install vyper~=0.4.0 - name: Forge RPC cache diff --git a/Cargo.lock b/Cargo.lock index ab47c2ef8cb1..9431ac1d325a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3757,9 +3757,9 @@ dependencies = [ [[package]] name = "foundry-compilers" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c6cc925fc9fdd73f1038c528fef17ddbdd7512311809ace7d1860fe3666dbb5" +checksum = "5cabcc146459af3ef3c7d05bfd1cca1450b065aaa0c168d567b3fe25d8530558" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -3795,9 +3795,9 @@ dependencies = [ [[package]] name = "foundry-compilers-artifacts" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d5c999c80c6d702c51522f5b4a805bec5fcae978637f0c337fa5c7a4b43d863" +checksum = "0217f21ad9c0aa8127fcca23e69bda25101b0849027e3cf949f04252b22810fd" dependencies = [ "foundry-compilers-artifacts-solc", "foundry-compilers-artifacts-vyper", @@ -3805,9 +3805,9 @@ dependencies = [ [[package]] name = "foundry-compilers-artifacts-solc" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3747cfeab1fc8299d70ceae0a28b7e2e005324e8eba78ac7d06729d67be5a1ec" +checksum = "e35255af997575a4aac46d86259fb43c222edfbcf48230ed929fa7c92ece6277" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -3829,9 +3829,9 @@ dependencies = [ [[package]] name = "foundry-compilers-artifacts-vyper" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbd5c142355bd4822b8a7ec37268cfafe37b2e36835fa8d067b2b9d5a22c7529" +checksum = "626fdc9a2ba83240d1b5ebbe3d8d50d794f231aa652abf27289119e71f6e774b" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -3844,9 +3844,9 @@ dependencies = [ [[package]] name = "foundry-compilers-core" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1291c05a4c8c3b4558eb1b50f53ee1f1b599ff2490d62cdc519ad5ae4b088d6" +checksum = "51486ab73194212079801418f6cc66ba05275467c80241a7f05bb7ef0475774a" dependencies = [ "alloy-primitives", "cfg-if", diff --git a/Cargo.toml b/Cargo.toml index 2bb8f894570b..877037e09716 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -169,7 +169,7 @@ foundry-linking = { path = "crates/linking" } # solc & compilation utilities foundry-block-explorers = { version = "0.7.3", default-features = false } -foundry-compilers = { version = "0.11.4", default-features = false } +foundry-compilers = { version = "0.11.5", default-features = false } foundry-fork-db = "0.4.0" solang-parser = "=0.3.3" diff --git a/crates/forge/bin/cmd/compiler.rs b/crates/forge/bin/cmd/compiler.rs new file mode 100644 index 000000000000..badc053c6c4d --- /dev/null +++ b/crates/forge/bin/cmd/compiler.rs @@ -0,0 +1,145 @@ +use clap::{ArgAction, Parser, Subcommand, ValueHint}; +use eyre::Result; +use foundry_compilers::Graph; +use foundry_config::Config; +use semver::Version; +use std::{collections::BTreeMap, path::PathBuf}; + +/// CLI arguments for `forge compiler`. +#[derive(Debug, Parser)] +pub struct CompilerArgs { + #[command(subcommand)] + pub sub: CompilerSubcommands, +} + +impl CompilerArgs { + pub fn run(self) -> Result<()> { + match self.sub { + CompilerSubcommands::Resolve(args) => args.run(), + } + } +} + +#[derive(Debug, Subcommand)] +pub enum CompilerSubcommands { + /// Retrieves the resolved version(s) of the compiler within the project. + #[command(visible_alias = "r")] + Resolve(ResolveArgs), +} + +/// CLI arguments for `forge compiler resolve`. +#[derive(Debug, Parser)] +pub struct ResolveArgs { + /// The root directory + #[arg(long, short, value_hint = ValueHint::DirPath, value_name = "PATH")] + root: Option, + + /// Skip files that match the given regex pattern. + #[arg(long, short, value_name = "REGEX")] + skip: Option, + + /// Verbosity of the output. + /// + /// Pass multiple times to increase the verbosity (e.g. -v, -vv, -vvv). + /// + /// Verbosity levels: + /// - 2: Print source paths. + #[arg(long, short, verbatim_doc_comment, action = ArgAction::Count, help_heading = "Display options")] + pub verbosity: u8, + + /// Print as JSON. + #[arg(long, short, help_heading = "Display options")] + json: bool, +} + +impl ResolveArgs { + pub fn run(self) -> Result<()> { + let Self { root, skip, verbosity, json } = self; + + let root = root.unwrap_or_else(|| PathBuf::from(".")); + let config = Config::load_with_root(&root); + let project = config.project()?; + + let graph = Graph::resolve(&project.paths)?; + let (sources, _) = graph.into_sources_by_version( + project.offline, + &project.locked_versions, + &project.compiler, + )?; + + let mut output: BTreeMap)>> = BTreeMap::new(); + + for (language, sources) in sources { + let mut versions_with_paths: Vec<(Version, Vec)> = sources + .iter() + .map(|(version, sources)| { + let paths: Vec = sources + .iter() + .filter_map(|(path_file, _)| { + let path_str = path_file + .strip_prefix(&project.paths.root) + .unwrap_or(path_file) + .to_path_buf() + .display() + .to_string(); + + // Skip files that match the given regex pattern. + if let Some(ref regex) = skip { + if regex.is_match(&path_str) { + return None; + } + } + + Some(path_str) + }) + .collect(); + + (version.clone(), paths) + }) + .filter(|(_, paths)| !paths.is_empty()) + .collect(); + + // Sort by SemVer version. + versions_with_paths.sort_by(|(v1, _), (v2, _)| Version::cmp(v1, v2)); + + // Skip language if no paths are found after filtering. + if !versions_with_paths.is_empty() { + output.insert(language.to_string(), versions_with_paths); + } + } + + if json { + println!("{}", serde_json::to_string(&output)?); + return Ok(()); + } + + for (language, versions) in &output { + if verbosity < 1 { + println!("{language}:"); + } else { + println!("{language}:\n"); + } + + for (version, paths) in versions { + if verbosity >= 1 { + println!("{version}:"); + for (idx, path) in paths.iter().enumerate() { + if idx == paths.len() - 1 { + println!("└── {path}\n"); + } else { + println!("├── {path}"); + } + } + } else { + println!("- {version}"); + } + } + + if verbosity < 1 { + println!(); + } + } + + Ok(()) + } +} diff --git a/crates/forge/bin/cmd/mod.rs b/crates/forge/bin/cmd/mod.rs index ff63fa7cbc0e..f2de1d6321d5 100644 --- a/crates/forge/bin/cmd/mod.rs +++ b/crates/forge/bin/cmd/mod.rs @@ -44,6 +44,7 @@ pub mod bind_json; pub mod build; pub mod cache; pub mod clone; +pub mod compiler; pub mod config; pub mod coverage; pub mod create; diff --git a/crates/forge/bin/main.rs b/crates/forge/bin/main.rs index ba89d6dcf632..925788ef8fda 100644 --- a/crates/forge/bin/main.rs +++ b/crates/forge/bin/main.rs @@ -118,6 +118,7 @@ fn main() -> Result<()> { ForgeSubcommand::Generate(cmd) => match cmd.sub { GenerateSubcommands::Test(cmd) => cmd.run(), }, + ForgeSubcommand::Compiler(cmd) => cmd.run(), ForgeSubcommand::Soldeer(cmd) => utils::block_on(cmd.run()), ForgeSubcommand::Eip712(cmd) => cmd.run(), ForgeSubcommand::BindJson(cmd) => cmd.run(), diff --git a/crates/forge/bin/opts.rs b/crates/forge/bin/opts.rs index c929d0185ba7..d0dfecd3a132 100644 --- a/crates/forge/bin/opts.rs +++ b/crates/forge/bin/opts.rs @@ -1,8 +1,9 @@ use crate::cmd::{ - bind::BindArgs, bind_json, build::BuildArgs, cache::CacheArgs, clone::CloneArgs, config, - coverage, create::CreateArgs, debug::DebugArgs, doc::DocArgs, eip712, flatten, fmt::FmtArgs, - geiger, generate, init::InitArgs, inspect, install::InstallArgs, remappings::RemappingArgs, - remove::RemoveArgs, selectors::SelectorsSubcommands, snapshot, soldeer, test, tree, update, + bind::BindArgs, bind_json, build::BuildArgs, cache::CacheArgs, clone::CloneArgs, + compiler::CompilerArgs, config, coverage, create::CreateArgs, debug::DebugArgs, doc::DocArgs, + eip712, flatten, fmt::FmtArgs, geiger, generate, init::InitArgs, inspect, install::InstallArgs, + remappings::RemappingArgs, remove::RemoveArgs, selectors::SelectorsSubcommands, snapshot, + soldeer, test, tree, update, }; use clap::{Parser, Subcommand, ValueHint}; use forge_script::ScriptArgs; @@ -152,7 +153,7 @@ pub enum ForgeSubcommand { /// Generate documentation for the project. Doc(DocArgs), - /// Function selector utilities + /// Function selector utilities. #[command(visible_alias = "se")] Selectors { #[command(subcommand)] @@ -162,6 +163,9 @@ pub enum ForgeSubcommand { /// Generate scaffold files. Generate(generate::GenerateArgs), + /// Compiler utilities. + Compiler(CompilerArgs), + /// Soldeer dependency manager. Soldeer(soldeer::SoldeerArgs), diff --git a/crates/forge/tests/cli/compiler.rs b/crates/forge/tests/cli/compiler.rs new file mode 100644 index 000000000000..665356632320 --- /dev/null +++ b/crates/forge/tests/cli/compiler.rs @@ -0,0 +1,218 @@ +//! Tests for the `forge compiler` command. + +use foundry_test_utils::snapbox::IntoData; + +const CONTRACT_A: &str = r#" +// SPDX-license-identifier: MIT +pragma solidity 0.8.4; + +contract ContractA {} +"#; + +const CONTRACT_B: &str = r#" +// SPDX-license-identifier: MIT +pragma solidity 0.8.11; + +contract ContractB {} +"#; + +const CONTRACT_C: &str = r#" +// SPDX-license-identifier: MIT +pragma solidity 0.8.27; + +contract ContractC {} +"#; + +const CONTRACT_D: &str = r#" +// SPDX-license-identifier: MIT +pragma solidity 0.8.27; + +contract ContractD {} +"#; + +const VYPER_INTERFACE: &str = r#" +# pragma version 0.4.0 + +@external +@view +def number() -> uint256: + return empty(uint256) + +@external +def set_number(new_number: uint256): + pass + +@external +def increment() -> uint256: + return empty(uint256) +"#; + +const VYPER_CONTRACT: &str = r#" +import ICounter +implements: ICounter + +number: public(uint256) + +@external +def set_number(new_number: uint256): + self.number = new_number + +@external +def increment() -> uint256: + self.number += 1 + return self.number +"#; + +forgetest!(can_resolve_path, |prj, cmd| { + prj.add_source("ContractA", CONTRACT_A).unwrap(); + + cmd.args(["compiler", "resolve", "--root", prj.root().to_str().unwrap()]) + .assert_success() + .stdout_eq(str![[r#" +Solidity: +- 0.8.4 + + +"#]]); +}); + +forgetest!(can_list_resolved_compiler_versions, |prj, cmd| { + prj.add_source("ContractA", CONTRACT_A).unwrap(); + + cmd.args(["compiler", "resolve"]).assert_success().stdout_eq(str![[r#" +Solidity: +- 0.8.4 + + +"#]]); +}); + +forgetest!(can_list_resolved_compiler_versions_verbose, |prj, cmd| { + prj.add_source("ContractC", CONTRACT_C).unwrap(); + prj.add_source("ContractD", CONTRACT_D).unwrap(); + + cmd.args(["compiler", "resolve", "-v"]).assert_success().stdout_eq(str![[r#" +Solidity: + +0.8.27: +├── src/ContractC.sol +└── src/ContractD.sol + + +"#]]); +}); + +forgetest!(can_list_resolved_compiler_versions_json, |prj, cmd| { + prj.add_source("ContractC", CONTRACT_C).unwrap(); + prj.add_source("ContractD", CONTRACT_D).unwrap(); + + cmd.args(["compiler", "resolve", "--json"]).assert_success().stdout_eq( + str![[r#" +{"Solidity":[["0.8.27",["src/ContractC.sol","src/ContractD.sol"]]]}"#]] + .is_json(), + ); +}); + +forgetest!(can_list_resolved_multiple_compiler_versions, |prj, cmd| { + prj.add_source("ContractA", CONTRACT_A).unwrap(); + prj.add_source("ContractB", CONTRACT_B).unwrap(); + prj.add_source("ContractC", CONTRACT_C).unwrap(); + prj.add_source("ContractD", CONTRACT_D).unwrap(); + prj.add_raw_source("ICounter.vyi", VYPER_INTERFACE).unwrap(); + prj.add_raw_source("Counter.vy", VYPER_CONTRACT).unwrap(); + + cmd.args(["compiler", "resolve"]).assert_success().stdout_eq(str![[r#" +Solidity: +- 0.8.4 +- 0.8.11 +- 0.8.27 + +Vyper: +- 0.4.0 + + +"#]]); +}); + +forgetest!(can_list_resolved_multiple_compiler_versions_skipped, |prj, cmd| { + prj.add_source("ContractA", CONTRACT_A).unwrap(); + prj.add_source("ContractB", CONTRACT_B).unwrap(); + prj.add_source("ContractC", CONTRACT_C).unwrap(); + prj.add_source("ContractD", CONTRACT_D).unwrap(); + prj.add_raw_source("ICounter.vyi", VYPER_INTERFACE).unwrap(); + prj.add_raw_source("Counter.vy", VYPER_CONTRACT).unwrap(); + + cmd.args(["compiler", "resolve", "--skip", ".sol", "-v"]).assert_success().stdout_eq(str![[ + r#" +Vyper: + +0.4.0: +├── src/Counter.vy +└── src/ICounter.vyi + + +"# + ]]); +}); + +forgetest!(can_list_resolved_multiple_compiler_versions_skipped_json, |prj, cmd| { + prj.add_source("ContractA", CONTRACT_A).unwrap(); + prj.add_source("ContractB", CONTRACT_B).unwrap(); + prj.add_source("ContractC", CONTRACT_C).unwrap(); + prj.add_source("ContractD", CONTRACT_D).unwrap(); + prj.add_raw_source("ICounter.vyi", VYPER_INTERFACE).unwrap(); + prj.add_raw_source("Counter.vy", VYPER_CONTRACT).unwrap(); + + cmd.args(["compiler", "resolve", "--skip", "Contract(A|B|C)", "--json"]) + .assert_success() + .stdout_eq(str![[r#" +{"Solidity":[["0.8.27",["src/ContractD.sol"]]],"Vyper":[["0.4.0",["src/Counter.vy","src/ICounter.vyi"]]]} +"#]].is_json()); +}); + +forgetest!(can_list_resolved_multiple_compiler_versions_verbose, |prj, cmd| { + prj.add_source("ContractA", CONTRACT_A).unwrap(); + prj.add_source("ContractB", CONTRACT_B).unwrap(); + prj.add_source("ContractC", CONTRACT_C).unwrap(); + prj.add_source("ContractD", CONTRACT_D).unwrap(); + prj.add_raw_source("ICounter.vyi", VYPER_INTERFACE).unwrap(); + prj.add_raw_source("Counter.vy", VYPER_CONTRACT).unwrap(); + + cmd.args(["compiler", "resolve", "-v"]).assert_success().stdout_eq(str![[r#" +Solidity: + +0.8.4: +└── src/ContractA.sol + +0.8.11: +└── src/ContractB.sol + +0.8.27: +├── src/ContractC.sol +└── src/ContractD.sol + +Vyper: + +0.4.0: +├── src/Counter.vy +└── src/ICounter.vyi + + +"#]]); +}); + +forgetest!(can_list_resolved_multiple_compiler_versions_json, |prj, cmd| { + prj.add_source("ContractA", CONTRACT_A).unwrap(); + prj.add_source("ContractB", CONTRACT_B).unwrap(); + prj.add_source("ContractC", CONTRACT_C).unwrap(); + prj.add_source("ContractD", CONTRACT_D).unwrap(); + prj.add_raw_source("ICounter.vyi", VYPER_INTERFACE).unwrap(); + prj.add_raw_source("Counter.vy", VYPER_CONTRACT).unwrap(); + + cmd.args(["compiler", "resolve", "--json"]).assert_success().stdout_eq( + str![[r#" +{"Solidity":[["0.8.4",["src/ContractA.sol"]],["0.8.11",["src/ContractB.sol"]],["0.8.27",["src/ContractC.sol","src/ContractD.sol"]]],"Vyper":[["0.4.0",["src/Counter.vy","src/ICounter.vyi"]]]} +"#]] + .is_json(), + ); +}); diff --git a/crates/forge/tests/cli/main.rs b/crates/forge/tests/cli/main.rs index 6ad29ca48a93..a53a26d2ac5a 100644 --- a/crates/forge/tests/cli/main.rs +++ b/crates/forge/tests/cli/main.rs @@ -9,6 +9,7 @@ mod bind_json; mod build; mod cache; mod cmd; +mod compiler; mod config; mod context; mod coverage;