瀏覽代碼

Add --contract and --output-meta CLI flags (#1164)

Add cli option --output-meta directory

This sets the directory for the Solana idl file, or Substrate contract file.

Add --contract command line option to specify which contracts we want

The use cases is in the anchor environment. We have a solidity file,
and a name of contract. If that contract is not present in the source
code, we want compilation to fail.

Signed-off-by: Sean Young <sean@mess.org>
Sean Young 2 年之前
父節點
當前提交
528676de7b
共有 6 個文件被更改,包括 240 次插入116 次删除
  1. 1 0
      Cargo.toml
  2. 19 4
      docs/running.rst
  3. 3 3
      src/abi/mod.rs
  4. 165 106
      src/bin/solang.rs
  5. 1 1
      tests/.gitignore
  6. 51 2
      tests/cli.rs

+ 1 - 0
Cargo.toml

@@ -75,6 +75,7 @@ path-slash = "0.2"
 pretty_assertions = "1.3"
 byte-slice-cast = "1.2"
 borsh = "0.10"
+tempfile = "3.3"
 rayon = "1"
 
 [package.metadata.docs.rs]

+ 19 - 4
docs/running.rst

@@ -47,8 +47,15 @@ Options:
   is ignored for any other target.
 
 -o, \-\-output *directory*
-  This option takes one argument, which is the directory where output should
-  be saved. The default is the current directory.
+  Sets the directory where the output should be saved. This defaults to the current working directory if not set.
+
+\-\-output\-meta *directory*
+  Sets the directory where metadata should be saved. For Solana, the metadata is the Anchor IDL file,
+  and, for Substrate, the .contract file. If this option is not set, the directory specified by ``--output``
+  is used, and if that is not set either, the current working directory is used.
+
+\-\-contract *contract-name* [, *contract-name*]...
+  Only compile the code for the specified contracts. If any those contracts cannot be found, produce an error.
 
 -O *optimization level*
   This takes one argument, which can either be ``none``, ``less``, ``default``,
@@ -56,7 +63,7 @@ Options:
 
 \-\-importpath *directory*
   When resolving ``import`` directives, search this directory. By default ``import``
-  will only search the current directory. This option can be specified multiple times
+  will only search the current working directory. This option can be specified multiple times
   and the directories will be searched in the order specified.
 
 \-\-importmap *map=directory*
@@ -115,6 +122,14 @@ Options:
 \-\-log\-api\-return\-codes
    Disable the :ref:`common-subexpression-elimination` optimization
 
+.. warning::
+
+    If multiple Solidity source files define the same contract name, you will get a single
+    compiled contract file for this contract name. As a result, you will only have a single
+    contract with the duplicate name without knowing from which Solidity file it originated.
+    Solang will not give a warning about this problem.
+
+
 Generating Documentation Usage
 ______________________________
 
@@ -143,7 +158,7 @@ Options:
 
 \-\-importpath *directory*
   When resolving ``import`` directives, search this directory. By default ``import``
-  will only search the current directory. This option can be specified multiple times
+  will only search the current working directory. This option can be specified multiple times
   and the directories will be searched in the order specified.
 
 \-\-importmap *map=directory*

+ 3 - 3
src/abi/mod.rs

@@ -18,14 +18,14 @@ pub fn generate_abi(
         Target::Substrate { .. } => {
             if verbose {
                 eprintln!(
-                    "info: Generating Substrate ABI for contract {}",
+                    "info: Generating Substrate metadata for contract {}",
                     ns.contracts[contract_no].name
                 );
             }
 
-            let abi = substrate::metadata(contract_no, code, ns);
+            let metadata = substrate::metadata(contract_no, code, ns);
 
-            (serde_json::to_string_pretty(&abi).unwrap(), "contract")
+            (serde_json::to_string_pretty(&metadata).unwrap(), "contract")
         }
         Target::Solana => {
             if verbose {

+ 165 - 106
src/bin/solang.rs

@@ -6,6 +6,7 @@ use clap::{
     value_parser, Arg, ArgMatches, Command,
 };
 use clap_complete::{generate, Shell};
+use itertools::Itertools;
 use solang::{
     abi,
     codegen::{codegen, OptimizationLevel, Options},
@@ -16,7 +17,7 @@ use solang::{
     Target,
 };
 use std::{
-    collections::HashMap,
+    collections::{HashMap, HashSet},
     ffi::{OsStr, OsString},
     fs::{create_dir_all, File},
     io::prelude::*,
@@ -56,6 +57,13 @@ fn main() {
                                 "ast-dot", "cfg", "llvm-ir", "llvm-bc", "object", "asm",
                             ]),
                     )
+                    .arg(
+                        Arg::new("CONTRACT")
+                            .help("Contract names to compile (defaults to all)")
+                            .value_delimiter(',')
+                            .action(ArgAction::Append)
+                            .long("contract"),
+                    )
                     .arg(
                         Arg::new("OPT")
                             .help("Set llvm optimizer level")
@@ -111,6 +119,13 @@ fn main() {
                             .num_args(1)
                             .value_parser(ValueParser::os_string()),
                     )
+                    .arg(
+                        Arg::new("OUTPUTMETA")
+                            .help("output directory for metadata")
+                            .long("output-meta")
+                            .num_args(1)
+                            .value_parser(ValueParser::os_string()),
+                    )
                     .arg(
                         Arg::new("IMPORTPATH")
                             .help("Directory to search for solidity files")
@@ -416,12 +431,35 @@ fn compile(matches: &ArgMatches) {
 
     let mut errors = false;
 
+    // Build a map of requested contract names, and a flag specifying whether it was found or not
+    let contract_names: HashSet<&str> = if let Some(values) = matches.get_many::<String>("CONTRACT")
+    {
+        values.map(|v| v.as_str()).collect()
+    } else {
+        HashSet::new()
+    };
+
     for filename in matches.get_many::<OsString>("INPUT").unwrap() {
-        match process_file(filename, &mut resolver, target, matches, &mut json, &opt) {
-            Ok(ns) => namespaces.push(ns),
-            Err(_) => {
-                errors = true;
-            }
+        // TODO: this could be parallelized using e.g. rayon
+        let ns = process_file(filename, &mut resolver, target, matches, &opt);
+
+        namespaces.push((ns, filename));
+    }
+
+    let mut json_contracts = HashMap::new();
+
+    let std_json = *matches.get_one("STD-JSON").unwrap();
+
+    for (ns, _) in &namespaces {
+        if std_json {
+            let mut out = ns.diagnostics_as_json(&resolver);
+            json.errors.append(&mut out);
+        } else {
+            ns.print_diagnostics(&resolver, verbose);
+        }
+
+        if ns.diagnostics.any_errors() {
+            errors = true;
         }
     }
 
@@ -429,20 +467,50 @@ fn compile(matches: &ArgMatches) {
         exit(0);
     }
 
-    let std_json = *matches.get_one("STD-JSON").unwrap();
+    // Ensure we have at least one contract
+    if !errors && namespaces.iter().all(|(ns, _)| ns.contracts.is_empty()) {
+        eprintln!("error: no contacts found");
+        errors = true;
+    }
 
-    if errors {
-        if std_json {
-            println!("{}", serde_json::to_string(&json).unwrap());
-            exit(0);
-        } else {
-            eprintln!("error: not all contracts are valid");
-            exit(1);
+    // Ensure we have all the requested contracts
+    let not_found: Vec<_> = contract_names
+        .iter()
+        .filter(|name| {
+            !namespaces
+                .iter()
+                .flat_map(|(ns, _)| ns.contracts.iter())
+                .any(|contract| **name == contract.name)
+        })
+        .collect();
+
+    if !errors && !not_found.is_empty() {
+        eprintln!("error: contacts {} not found", not_found.iter().join(", "));
+        errors = true;
+    }
+
+    if !errors {
+        for (ns, filename) in &namespaces {
+            for contract_no in 0..ns.contracts.len() {
+                contract_results(
+                    contract_no,
+                    filename,
+                    matches,
+                    ns,
+                    &mut json_contracts,
+                    &opt,
+                );
+            }
         }
     }
 
     if std_json {
         println!("{}", serde_json::to_string(&json).unwrap());
+        exit(0);
+    }
+
+    if errors {
+        exit(1);
     }
 }
 
@@ -455,13 +523,15 @@ fn shell_complete(mut app: Command, matches: &ArgMatches) {
     }
 }
 
-fn output_file(matches: &ArgMatches, stem: &str, ext: &str) -> PathBuf {
-    Path::new(
+fn output_file(matches: &ArgMatches, stem: &str, ext: &str, meta: bool) -> PathBuf {
+    let dir = if meta {
         matches
-            .get_one::<OsString>("OUTPUT")
-            .unwrap_or(&OsString::from(".")),
-    )
-    .join(format!("{stem}.{ext}"))
+            .get_one::<OsString>("OUTPUTMETA")
+            .or_else(|| matches.get_one::<OsString>("OUTPUT"))
+    } else {
+        matches.get_one::<OsString>("OUTPUT")
+    };
+    Path::new(dir.unwrap_or(&OsString::from("."))).join(format!("{stem}.{ext}"))
 }
 
 fn process_file(
@@ -469,13 +539,9 @@ fn process_file(
     resolver: &mut FileResolver,
     target: solang::Target,
     matches: &ArgMatches,
-    json: &mut JsonResult,
     opt: &Options,
-) -> Result<Namespace, ()> {
+) -> Namespace {
     let verbose = *matches.get_one("VERBOSE").unwrap();
-    let std_json = *matches.get_one("STD-JSON").unwrap();
-
-    let mut json_contracts = HashMap::new();
 
     // resolve phase
     let mut ns = solang::parse_and_resolve(filename, resolver, target);
@@ -483,17 +549,10 @@ fn process_file(
     // codegen all the contracts; some additional errors/warnings will be detected here
     codegen(&mut ns, opt);
 
-    if std_json {
-        let mut out = ns.diagnostics_as_json(resolver);
-        json.errors.append(&mut out);
-    } else {
-        ns.print_diagnostics(resolver, verbose);
-    }
-
     if let Some("ast-dot") = matches.get_one::<String>("EMIT").map(|v| v.as_str()) {
         let filepath = PathBuf::from(filename);
         let stem = filepath.file_stem().unwrap().to_string_lossy();
-        let dot_filename = output_file(matches, &stem, "dot");
+        let dot_filename = output_file(matches, &stem, "dot", false);
 
         if verbose {
             eprintln!("info: Saving graphviz dot {}", dot_filename.display());
@@ -507,98 +566,98 @@ fn process_file(
             eprintln!("{}: error: {}", dot_filename.display(), err);
             exit(1);
         }
-
-        return Ok(ns);
     }
 
-    if ns.contracts.is_empty() || ns.diagnostics.any_errors() {
-        return Err(());
-    }
+    ns
+}
 
-    // emit phase
-    for contract_no in 0..ns.contracts.len() {
-        let resolved_contract = &ns.contracts[contract_no];
+fn contract_results(
+    contract_no: usize,
+    filename: &OsStr,
+    matches: &ArgMatches,
+    ns: &Namespace,
+    json_contracts: &mut HashMap<String, JsonContract>,
+    opt: &Options,
+) {
+    let verbose = *matches.get_one("VERBOSE").unwrap();
+    let std_json = *matches.get_one("STD-JSON").unwrap();
 
-        if !resolved_contract.instantiable {
-            continue;
-        }
+    let resolved_contract = &ns.contracts[contract_no];
 
-        if let Some("cfg") = matches.get_one::<String>("EMIT").map(|v| v.as_str()) {
-            println!("{}", resolved_contract.print_cfg(&ns));
-            continue;
-        }
+    if !resolved_contract.instantiable {
+        return;
+    }
 
-        if verbose {
-            if target == solang::Target::Solana {
-                eprintln!(
-                    "info: contract {} uses at least {} bytes account data",
-                    resolved_contract.name, resolved_contract.fixed_layout_size,
-                );
-            }
+    if let Some("cfg") = matches.get_one::<String>("EMIT").map(|v| v.as_str()) {
+        println!("{}", resolved_contract.print_cfg(ns));
+        return;
+    }
 
+    if verbose {
+        if ns.target == solang::Target::Solana {
             eprintln!(
-                "info: Generating LLVM IR for contract {} with target {}",
-                resolved_contract.name, ns.target
+                "info: contract {} uses at least {} bytes account data",
+                resolved_contract.name, resolved_contract.fixed_layout_size,
             );
         }
 
-        let context = inkwell::context::Context::create();
-        let filename_string = filename.to_string_lossy();
+        eprintln!(
+            "info: Generating LLVM IR for contract {} with target {}",
+            resolved_contract.name, ns.target
+        );
+    }
 
-        let binary = resolved_contract.binary(&ns, &context, &filename_string, opt);
+    let context = inkwell::context::Context::create();
+    let filename_string = filename.to_string_lossy();
 
-        if save_intermediates(&binary, matches) {
-            continue;
-        }
+    let binary = resolved_contract.binary(ns, &context, &filename_string, opt);
 
-        let code = binary.code(Generate::Linked).expect("llvm build");
+    if save_intermediates(&binary, matches) {
+        return;
+    }
 
-        if std_json {
-            json_contracts.insert(
-                binary.name.to_owned(),
-                JsonContract {
-                    abi: abi::ethereum::gen_abi(contract_no, &ns),
-                    ewasm: Some(EwasmContract {
-                        wasm: hex::encode_upper(code),
-                    }),
-                    minimum_space: None,
-                },
-            );
-        } else {
-            let bin_filename = output_file(matches, &binary.name, target.file_extension());
+    let code = binary.code(Generate::Linked).expect("llvm build");
 
-            if verbose {
-                eprintln!(
-                    "info: Saving binary {} for contract {}",
-                    bin_filename.display(),
-                    binary.name
-                );
-            }
+    if std_json {
+        json_contracts.insert(
+            binary.name,
+            JsonContract {
+                abi: abi::ethereum::gen_abi(contract_no, ns),
+                ewasm: Some(EwasmContract {
+                    wasm: hex::encode_upper(code),
+                }),
+                minimum_space: None,
+            },
+        );
+    } else {
+        let bin_filename = output_file(matches, &binary.name, ns.target.file_extension(), false);
 
-            let mut file = create_file(&bin_filename);
+        if verbose {
+            eprintln!(
+                "info: Saving binary {} for contract {}",
+                bin_filename.display(),
+                binary.name
+            );
+        }
 
-            file.write_all(&code).unwrap();
+        let mut file = create_file(&bin_filename);
 
-            let (abi_bytes, abi_ext) = abi::generate_abi(contract_no, &ns, &code, verbose);
-            let abi_filename = output_file(matches, &binary.name, abi_ext);
+        file.write_all(&code).unwrap();
 
-            if verbose {
-                eprintln!(
-                    "info: Saving metadata {} for contract {}",
-                    abi_filename.display(),
-                    binary.name
-                );
-            }
+        let (metadata, meta_ext) = abi::generate_abi(contract_no, ns, &code, verbose);
+        let meta_filename = output_file(matches, &binary.name, meta_ext, true);
 
-            let mut file = create_file(&abi_filename);
-            file.write_all(abi_bytes.as_bytes()).unwrap();
+        if verbose {
+            eprintln!(
+                "info: Saving metadata {} for contract {}",
+                meta_filename.display(),
+                binary.name
+            );
         }
-    }
 
-    json.contracts
-        .insert(filename.to_string_lossy().to_string(), json_contracts);
-
-    Ok(ns)
+        let mut file = create_file(&meta_filename);
+        file.write_all(metadata.as_bytes()).unwrap();
+    }
 }
 
 fn save_intermediates(binary: &solang::emit::binary::Binary, matches: &ArgMatches) -> bool {
@@ -606,7 +665,7 @@ fn save_intermediates(binary: &solang::emit::binary::Binary, matches: &ArgMatche
 
     match matches.get_one::<String>("EMIT").map(|v| v.as_str()) {
         Some("llvm-ir") => {
-            let llvm_filename = output_file(matches, &binary.name, "ll");
+            let llvm_filename = output_file(matches, &binary.name, "ll", false);
 
             if verbose {
                 eprintln!(
@@ -622,7 +681,7 @@ fn save_intermediates(binary: &solang::emit::binary::Binary, matches: &ArgMatche
         }
 
         Some("llvm-bc") => {
-            let bc_filename = output_file(matches, &binary.name, "bc");
+            let bc_filename = output_file(matches, &binary.name, "bc", false);
 
             if verbose {
                 eprintln!(
@@ -646,7 +705,7 @@ fn save_intermediates(binary: &solang::emit::binary::Binary, matches: &ArgMatche
                 }
             };
 
-            let obj_filename = output_file(matches, &binary.name, "o");
+            let obj_filename = output_file(matches, &binary.name, "o", false);
 
             if verbose {
                 eprintln!(
@@ -669,7 +728,7 @@ fn save_intermediates(binary: &solang::emit::binary::Binary, matches: &ArgMatche
                 }
             };
 
-            let obj_filename = output_file(matches, &binary.name, "asm");
+            let obj_filename = output_file(matches, &binary.name, "asm", false);
 
             if verbose {
                 eprintln!(

+ 1 - 1
tests/.gitignore

@@ -1,4 +1,4 @@
 *.wasm
-*.abi
+*.json
 *.so
 *.contract

+ 51 - 2
tests/cli.rs

@@ -2,23 +2,52 @@
 
 use assert_cmd::Command;
 use std::fs::File;
+use tempfile::TempDir;
 
 #[test]
 fn create_output_dir() {
     let mut cmd = Command::cargo_bin("solang").unwrap();
 
+    let tmp = TempDir::new_in("tests").unwrap();
+
+    let test1 = tmp.path().join("test1");
+
+    cmd.args([
+        "compile",
+        "examples/flipper.sol",
+        "--target",
+        "solana",
+        "--output",
+    ])
+    .arg(test1.clone())
+    .assert()
+    .success();
+
+    File::open(test1.join("flipper.json")).expect("should exist");
+    File::open(test1.join("flipper.so")).expect("should exist");
+
+    let mut cmd = Command::cargo_bin("solang").unwrap();
+
+    let test2 = tmp.path().join("test2");
+    let test2_meta = tmp.path().join("test2_meta");
+
     cmd.args([
         "compile",
         "examples/flipper.sol",
         "--target",
         "solana",
+        "--contract",
+        "flipper",
         "--output",
-        "tests/create_me",
     ])
+    .arg(test2.clone())
+    .arg("--output-meta")
+    .arg(test2_meta.clone())
     .assert()
     .success();
 
-    File::open("tests/create_me/flipper.json").expect("should exist");
+    File::open(test2.join("flipper.so")).expect("should exist");
+    File::open(test2_meta.join("flipper.json")).expect("should exist");
 
     let mut cmd = Command::cargo_bin("solang").unwrap();
 
@@ -32,4 +61,24 @@ fn create_output_dir() {
     ])
     .assert()
     .failure();
+
+    let mut cmd = Command::cargo_bin("solang").unwrap();
+
+    let test3 = tmp.path().join("test3");
+
+    cmd.args([
+        "compile",
+        "examples/flipper.sol",
+        "--target",
+        "solana",
+        "--contract",
+        "flapper,flipper", // not just flipper
+        "--output",
+    ])
+    .arg(test3.clone())
+    .assert()
+    .failure();
+
+    // nothing should have been created because flapper does not exist
+    assert!(!test3.exists());
 }