Forráskód Böngészése

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 éve
szülő
commit
528676de7b
6 módosított fájl, 240 hozzáadás és 116 törlés
  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"
 pretty_assertions = "1.3"
 byte-slice-cast = "1.2"
 byte-slice-cast = "1.2"
 borsh = "0.10"
 borsh = "0.10"
+tempfile = "3.3"
 rayon = "1"
 rayon = "1"
 
 
 [package.metadata.docs.rs]
 [package.metadata.docs.rs]

+ 19 - 4
docs/running.rst

@@ -47,8 +47,15 @@ Options:
   is ignored for any other target.
   is ignored for any other target.
 
 
 -o, \-\-output *directory*
 -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*
 -O *optimization level*
   This takes one argument, which can either be ``none``, ``less``, ``default``,
   This takes one argument, which can either be ``none``, ``less``, ``default``,
@@ -56,7 +63,7 @@ Options:
 
 
 \-\-importpath *directory*
 \-\-importpath *directory*
   When resolving ``import`` directives, search this directory. By default ``import``
   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.
   and the directories will be searched in the order specified.
 
 
 \-\-importmap *map=directory*
 \-\-importmap *map=directory*
@@ -115,6 +122,14 @@ Options:
 \-\-log\-api\-return\-codes
 \-\-log\-api\-return\-codes
    Disable the :ref:`common-subexpression-elimination` optimization
    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
 Generating Documentation Usage
 ______________________________
 ______________________________
 
 
@@ -143,7 +158,7 @@ Options:
 
 
 \-\-importpath *directory*
 \-\-importpath *directory*
   When resolving ``import`` directives, search this directory. By default ``import``
   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.
   and the directories will be searched in the order specified.
 
 
 \-\-importmap *map=directory*
 \-\-importmap *map=directory*

+ 3 - 3
src/abi/mod.rs

@@ -18,14 +18,14 @@ pub fn generate_abi(
         Target::Substrate { .. } => {
         Target::Substrate { .. } => {
             if verbose {
             if verbose {
                 eprintln!(
                 eprintln!(
-                    "info: Generating Substrate ABI for contract {}",
+                    "info: Generating Substrate metadata for contract {}",
                     ns.contracts[contract_no].name
                     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 => {
         Target::Solana => {
             if verbose {
             if verbose {

+ 165 - 106
src/bin/solang.rs

@@ -6,6 +6,7 @@ use clap::{
     value_parser, Arg, ArgMatches, Command,
     value_parser, Arg, ArgMatches, Command,
 };
 };
 use clap_complete::{generate, Shell};
 use clap_complete::{generate, Shell};
+use itertools::Itertools;
 use solang::{
 use solang::{
     abi,
     abi,
     codegen::{codegen, OptimizationLevel, Options},
     codegen::{codegen, OptimizationLevel, Options},
@@ -16,7 +17,7 @@ use solang::{
     Target,
     Target,
 };
 };
 use std::{
 use std::{
-    collections::HashMap,
+    collections::{HashMap, HashSet},
     ffi::{OsStr, OsString},
     ffi::{OsStr, OsString},
     fs::{create_dir_all, File},
     fs::{create_dir_all, File},
     io::prelude::*,
     io::prelude::*,
@@ -56,6 +57,13 @@ fn main() {
                                 "ast-dot", "cfg", "llvm-ir", "llvm-bc", "object", "asm",
                                 "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(
                         Arg::new("OPT")
                         Arg::new("OPT")
                             .help("Set llvm optimizer level")
                             .help("Set llvm optimizer level")
@@ -111,6 +119,13 @@ fn main() {
                             .num_args(1)
                             .num_args(1)
                             .value_parser(ValueParser::os_string()),
                             .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(
                         Arg::new("IMPORTPATH")
                         Arg::new("IMPORTPATH")
                             .help("Directory to search for solidity files")
                             .help("Directory to search for solidity files")
@@ -416,12 +431,35 @@ fn compile(matches: &ArgMatches) {
 
 
     let mut errors = false;
     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() {
     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);
         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 {
     if std_json {
         println!("{}", serde_json::to_string(&json).unwrap());
         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
         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(
 fn process_file(
@@ -469,13 +539,9 @@ fn process_file(
     resolver: &mut FileResolver,
     resolver: &mut FileResolver,
     target: solang::Target,
     target: solang::Target,
     matches: &ArgMatches,
     matches: &ArgMatches,
-    json: &mut JsonResult,
     opt: &Options,
     opt: &Options,
-) -> Result<Namespace, ()> {
+) -> Namespace {
     let verbose = *matches.get_one("VERBOSE").unwrap();
     let verbose = *matches.get_one("VERBOSE").unwrap();
-    let std_json = *matches.get_one("STD-JSON").unwrap();
-
-    let mut json_contracts = HashMap::new();
 
 
     // resolve phase
     // resolve phase
     let mut ns = solang::parse_and_resolve(filename, resolver, target);
     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 all the contracts; some additional errors/warnings will be detected here
     codegen(&mut ns, opt);
     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()) {
     if let Some("ast-dot") = matches.get_one::<String>("EMIT").map(|v| v.as_str()) {
         let filepath = PathBuf::from(filename);
         let filepath = PathBuf::from(filename);
         let stem = filepath.file_stem().unwrap().to_string_lossy();
         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 {
         if verbose {
             eprintln!("info: Saving graphviz dot {}", dot_filename.display());
             eprintln!("info: Saving graphviz dot {}", dot_filename.display());
@@ -507,98 +566,98 @@ fn process_file(
             eprintln!("{}: error: {}", dot_filename.display(), err);
             eprintln!("{}: error: {}", dot_filename.display(), err);
             exit(1);
             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!(
             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 {
 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()) {
     match matches.get_one::<String>("EMIT").map(|v| v.as_str()) {
         Some("llvm-ir") => {
         Some("llvm-ir") => {
-            let llvm_filename = output_file(matches, &binary.name, "ll");
+            let llvm_filename = output_file(matches, &binary.name, "ll", false);
 
 
             if verbose {
             if verbose {
                 eprintln!(
                 eprintln!(
@@ -622,7 +681,7 @@ fn save_intermediates(binary: &solang::emit::binary::Binary, matches: &ArgMatche
         }
         }
 
 
         Some("llvm-bc") => {
         Some("llvm-bc") => {
-            let bc_filename = output_file(matches, &binary.name, "bc");
+            let bc_filename = output_file(matches, &binary.name, "bc", false);
 
 
             if verbose {
             if verbose {
                 eprintln!(
                 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 {
             if verbose {
                 eprintln!(
                 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 {
             if verbose {
                 eprintln!(
                 eprintln!(

+ 1 - 1
tests/.gitignore

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

+ 51 - 2
tests/cli.rs

@@ -2,23 +2,52 @@
 
 
 use assert_cmd::Command;
 use assert_cmd::Command;
 use std::fs::File;
 use std::fs::File;
+use tempfile::TempDir;
 
 
 #[test]
 #[test]
 fn create_output_dir() {
 fn create_output_dir() {
     let mut cmd = Command::cargo_bin("solang").unwrap();
     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([
     cmd.args([
         "compile",
         "compile",
         "examples/flipper.sol",
         "examples/flipper.sol",
         "--target",
         "--target",
         "solana",
         "solana",
+        "--contract",
+        "flipper",
         "--output",
         "--output",
-        "tests/create_me",
     ])
     ])
+    .arg(test2.clone())
+    .arg("--output-meta")
+    .arg(test2_meta.clone())
     .assert()
     .assert()
     .success();
     .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();
     let mut cmd = Command::cargo_bin("solang").unwrap();
 
 
@@ -32,4 +61,24 @@ fn create_output_dir() {
     ])
     ])
     .assert()
     .assert()
     .failure();
     .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());
 }
 }