use clap::{
builder::ValueParser, parser::ValueSource, value_parser, ArgAction, ArgMatches, Args, Id,
Parser, Subcommand,
};
use clap_complete::Shell;
#[cfg(feature = "wasm_opt")]
use contract_build::OptimizationPasses;
use itertools::Itertools;
use semver::Version;
use serde::Deserialize;
use solang::{
codegen::{OptimizationLevel, Options},
file_resolver::FileResolver,
Target,
};
use std::{ffi::OsString, path::PathBuf, process::exit};
mod test;
#[derive(Parser)]
#[command(author = env!("CARGO_PKG_AUTHORS"), version = concat!("version ", env!("SOLANG_VERSION")), about = env!("CARGO_PKG_DESCRIPTION"), subcommand_required = true)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
#[command(about = "Compile Solidity source files")]
Compile(Compile),
#[command(about = "Generate documention for contracts using doc comments")]
Doc(Doc),
#[command(about = "Print shell completion for various shells to STDOUT")]
ShellComplete(ShellComplete),
#[cfg(feature = "language_server")]
#[command(about = "Start LSP language server on stdin/stdout")]
LanguageServer(LanguageServerCommand),
#[command(about = "Generate Solidity interface files from Anchor IDL files")]
Idl(IdlCommand),
#[command(about = "Create a new Solang project")]
New(New),
}
#[derive(Args)]
pub struct New {
#[arg(name = "TARGETNAME",required= true, long = "target", value_parser = ["solana", "polkadot", "evm"], help = "Target to build for [possible values: solana, polkadot]", num_args = 1, hide_possible_values = true)]
pub target_name: String,
#[arg(name = "INPUT", help = "Name of the project", num_args = 1, value_parser = ValueParser::os_string())]
pub project_name: Option<OsString>,
}
#[derive(Args)]
pub struct IdlCommand {
#[arg(name = "INPUT", help = "Convert IDL files", required= true, value_parser = ValueParser::os_string(), num_args = 1..)]
pub input: Vec<OsString>,
#[arg(name = "OUTPUT",help = "output file", short = 'o', long = "output", num_args = 1, value_parser =ValueParser::path_buf())]
pub output: Option<PathBuf>,
}
#[derive(Args)]
pub struct LanguageServerCommand {
#[clap(flatten)]
pub target: TargetArg,
#[arg(name = "IMPORTPATH", help = "Directory to search for solidity files", value_parser = ValueParser::path_buf(), action = ArgAction::Append, long = "importpath", short = 'I', num_args = 1)]
pub import_path: Option<Vec<PathBuf>>,
#[arg(name = "IMPORTMAP", help = "Map directory to search for solidity files [format: map=path]",value_parser = ValueParser::new(parse_import_map), action = ArgAction::Append, long = "importmap", short = 'm', num_args = 1)]
pub import_map: Option<Vec<(String, PathBuf)>>,
}
#[derive(Args)]
pub struct ShellComplete {
#[arg(required = true, value_parser = value_parser!(Shell), help = "Name of a supported shell")]
pub shell_complete: Shell,
}
#[derive(Args)]
pub struct Doc {
#[clap(flatten)]
pub package: DocPackage,
#[clap(flatten)]
pub target: TargetArg,
#[arg(name = "VERBOSE" ,help = "show debug messages", short = 'v', action = ArgAction::SetTrue, long = "verbose")]
pub verbose: bool,
#[arg(name = "OUTPUT",help = "output directory", short = 'o', long = "output", num_args = 1, value_parser =ValueParser::string())]
pub output_directory: Option<OsString>,
}
#[derive(Args, Deserialize, Debug, PartialEq)]
pub struct Compile {
#[arg(name = "CONFFILE", help = "Take arguments from configuration file", long = "config-file", value_parser = ValueParser::os_string(), num_args = 0..=1, default_value = "solang.toml")]
#[serde(skip)]
pub configuration_file: Option<OsString>,
#[clap(flatten)]
pub package: CompilePackage,
#[clap(flatten)]
#[serde(
default = "CompilerOutput::default",
rename(deserialize = "compiler-output")
)]
pub compiler_output: CompilerOutput,
#[clap(flatten)]
#[serde(rename(deserialize = "target"))]
pub target_arg: CompileTargetArg,
#[clap(flatten)]
#[serde(default = "DebugFeatures::default")]
pub debug_features: DebugFeatures,
#[clap(flatten)]
#[serde(default = "Optimizations::default")]
pub optimizations: Optimizations,
}
impl Compile {
pub fn overwrite_with_matches(&mut self, matches: &ArgMatches) -> &mut Compile {
for id in explicit_args(matches) {
match id.as_str() {
"INPUT" => {
self.package.input = matches
.get_many::<PathBuf>("INPUT")
.map(|input_paths| input_paths.map(PathBuf::from).collect())
}
"CONTRACT" => {
self.package.contracts = matches
.get_many::<String>("CONTRACT")
.map(|contract_names| contract_names.map(String::from).collect())
}
"IMPORTPATH" => {
self.package.import_path = matches
.get_many::<PathBuf>("IMPORTPATH")
.map(|paths| paths.map(PathBuf::from).collect())
}
"IMPORTMAP" => {
self.package.import_map = matches
.get_many::<(String, PathBuf)>("IMPORTMAP")
.map(|import_map| import_map.cloned().collect())
}
"AUTHOR" => {
self.package.authors = matches
.get_many::<String>("AUTHOR")
.map(|contract_names| contract_names.map(String::from).collect())
}
"VERSION" => self.package.version = matches.get_one::<String>("VERSION").cloned(),
"EMIT" => self.compiler_output.emit = matches.get_one::<String>("EMIT").cloned(),
"OUTPUT" => {
self.compiler_output.output_directory =
matches.get_one::<String>("OUTPUT").cloned()
}
"OUTPUTMETA" => {
self.compiler_output.output_meta =
matches.get_one::<String>("OUTPUTMETA").cloned()
}
"STD-JSON" => {
self.compiler_output.std_json_output =
*matches.get_one::<bool>("STD-JSON").unwrap()
}
"VERBOSE" => {
self.compiler_output.verbose = *matches.get_one::<bool>("VERBOSE").unwrap()
}
"NOLOGRUNTIMEERRORS" => {
self.debug_features.log_runtime_errors =
*matches.get_one::<bool>("NOLOGRUNTIMEERRORS").unwrap()
}
"NOPRINTS" => {
self.debug_features.log_prints = *matches.get_one::<bool>("NOPRINTS").unwrap()
}
"GENERATEDEBUGINFORMATION" => {
self.debug_features.generate_debug_info =
*matches.get_one::<bool>("GENERATEDEBUGINFORMATION").unwrap()
}
"RELEASE" => {
self.debug_features.release = *matches.get_one::<bool>("RELEASE").unwrap()
}
"DEADSTORAGE" => {
self.optimizations.dead_storage =
*matches.get_one::<bool>("DEADSTORAGE").unwrap()
}
"CONSTANTFOLDING" => {
self.optimizations.constant_folding =
*matches.get_one::<bool>("CONSTANTFOLDING").unwrap()
}
"STRENGTHREDUCE" => {
self.optimizations.strength_reduce =
*matches.get_one::<bool>("STRENGTHREDUCE").unwrap()
}
"VECTORTOSLICE" => {
self.optimizations.vector_to_slice =
*matches.get_one::<bool>("VECTORTOSLICE").unwrap()
}
"COMMONSUBEXPRESSIONELIMINATION" => {
self.optimizations.common_subexpression_elimination = *matches
.get_one::<bool>("COMMONSUBEXPRESSIONELIMINATION")
.unwrap()
}
"OPT" => self.optimizations.opt_level = matches.get_one::<String>("OPT").cloned(),
"TARGET" => self.target_arg.name = matches.get_one::<String>("TARGET").cloned(),
"ADDRESS_LENGTH" => {
self.target_arg.address_length =
matches.get_one::<u64>("ADDRESS_LENGTH").copied()
}
"VALUE_LENGTH" => {
self.target_arg.value_length = matches.get_one::<u64>("VALUE_LENGTH").copied()
}
_ => {}
}
}
self
}
}
#[derive(Args, Deserialize, Default, Debug, PartialEq)]
pub struct CompilerOutput {
#[arg(name = "EMIT", help = "Emit compiler state at early stage", long = "emit", num_args = 1, value_parser = ["ast-dot", "cfg", "llvm-ir", "llvm-bc", "object", "asm"])]
#[serde(deserialize_with = "deserialize_emit", default)]
pub emit: Option<String>,
#[arg(name = "STD-JSON",help = "mimic solidity json output on stdout", conflicts_with_all = ["VERBOSE", "OUTPUT", "EMIT"], action = ArgAction::SetTrue, long = "standard-json")]
#[serde(default)]
pub std_json_output: bool,
#[arg(name = "OUTPUT",help = "output directory", short = 'o', long = "output", num_args = 1, value_parser =ValueParser::string())]
#[serde(default)]
pub output_directory: Option<String>,
#[arg(name = "OUTPUTMETA",help = "output directory for metadata", long = "output-meta", num_args = 1, value_parser = ValueParser::string())]
#[serde(default)]
pub output_meta: Option<String>,
#[arg(name = "VERBOSE" ,help = "show debug messages", short = 'v', action = ArgAction::SetTrue, long = "verbose")]
#[serde(default)]
pub verbose: bool,
}
#[derive(Args)]
pub struct TargetArg {
#[arg(name = "TARGET",required= true, long = "target", value_parser = ["solana", "polkadot", "evm"], help = "Target to build for [possible values: solana, polkadot]", num_args = 1, hide_possible_values = true)]
pub name: String,
#[arg(name = "ADDRESS_LENGTH", help = "Address length on the Polkadot Parachain", long = "address-length", num_args = 1, value_parser = value_parser!(u64).range(4..1024))]
pub address_length: Option<u64>,
#[arg(name = "VALUE_LENGTH", help = "Value length on the Polkadot Parachain", long = "value-length", num_args = 1, value_parser = value_parser!(u64).range(4..1024))]
pub value_length: Option<u64>,
}
#[derive(Args, Deserialize, Debug, PartialEq)]
pub struct CompileTargetArg {
#[arg(name = "TARGET", long = "target", value_parser = ["solana", "polkadot", "evm", "soroban"], help = "Target to build for [possible values: solana, polkadot]", num_args = 1, hide_possible_values = true)]
pub name: Option<String>,
#[arg(name = "ADDRESS_LENGTH", help = "Address length on the Polkadot Parachain", long = "address-length", num_args = 1, value_parser = value_parser!(u64).range(4..1024))]
pub address_length: Option<u64>,
#[arg(name = "VALUE_LENGTH", help = "Value length on the Polkadot Parachain", long = "value-length", num_args = 1, value_parser = value_parser!(u64).range(4..1024))]
pub value_length: Option<u64>,
}
#[derive(Args)]
pub struct DocPackage {
#[arg(name = "INPUT", help = "Solidity input files",value_parser = ValueParser::path_buf(), num_args = 1.., required = true)]
pub input: Vec<PathBuf>,
#[arg(name = "CONTRACT", help = "Contract names to compile (defaults to all)", value_delimiter = ',', action = ArgAction::Append, long = "contract")]
pub contracts: Option<Vec<String>>,
#[arg(name = "IMPORTPATH", help = "Directory to search for solidity files",value_parser = ValueParser::path_buf(), action = ArgAction::Append, long = "importpath", short = 'I', num_args = 1)]
pub import_path: Option<Vec<PathBuf>>,
#[arg(name = "IMPORTMAP", help = "Map directory to search for solidity files [format: map=path]",value_parser = ValueParser::new(parse_import_map), action = ArgAction::Append, long = "importmap", short = 'm', num_args = 1)]
pub import_map: Option<Vec<(String, PathBuf)>>,
}
#[derive(Args, Deserialize, Debug, PartialEq)]
pub struct CompilePackage {
#[arg(name = "INPUT", help = "Solidity input files",value_parser = ValueParser::path_buf(), num_args = 1..)]
#[serde(rename(deserialize = "input_files"))]
pub input: Option<Vec<PathBuf>>,
#[arg(name = "CONTRACT", help = "Contract names to compile (defaults to all)", value_delimiter = ',', action = ArgAction::Append, long = "contract")]
pub contracts: Option<Vec<String>>,
#[arg(name = "IMPORTPATH", help = "Directory to search for solidity files", value_parser = ValueParser::path_buf(), action = ArgAction::Append, long = "importpath", short = 'I', num_args = 1)]
pub import_path: Option<Vec<PathBuf>>,
#[arg(name = "IMPORTMAP", help = "Map directory to search for solidity files [format: map=path]",value_parser = ValueParser::new(parse_import_map), action = ArgAction::Append, long = "importmap", short = 'm', num_args = 1)]
#[serde(deserialize_with = "deserialize_inline_table", default)]
pub import_map: Option<Vec<(String, PathBuf)>>,
#[arg(name = "AUTHOR", help = "specify contracts authors", long = "contract-authors", value_delimiter = ',', action = ArgAction::Append)]
#[serde(default)]
pub authors: Option<Vec<String>>,
#[arg(name = "VERSION", help = "specify contracts version", long = "version", num_args = 1, value_parser = ValueParser::new(parse_version))]
#[serde(default, deserialize_with = "deserialize_version")]
pub version: Option<String>,
#[arg(
name = "SOROBAN-VERSION",
help = "specify soroban contracts pre-release number",
short = 's',
long = "soroban-version",
num_args = 1
)]
pub soroban_version: Option<u64>,
}
#[derive(Args, Deserialize, Debug, PartialEq)]
pub struct DebugFeatures {
#[arg(name = "NOLOGRUNTIMEERRORS", help = "Disable logging runtime errors in the environment", long = "no-log-runtime-errors", action = ArgAction::SetFalse)]
#[serde(default, rename(deserialize = "log-runtime-errors"))]
pub log_runtime_errors: bool,
#[arg(name = "NOPRINTS", help = "Disable logging prints in the environment", long = "no-prints", action = ArgAction::SetFalse)]
#[serde(default = "default_true", rename(deserialize = "prints"))]
pub log_prints: bool,
#[arg(name = "GENERATEDEBUGINFORMATION", help = "Enable generating debug information for LLVM IR", long = "generate-debug-info", action = ArgAction::SetTrue, short = 'g')]
#[serde(default, rename(deserialize = "generate-debug-info"))]
pub generate_debug_info: bool,
#[arg(name = "RELEASE", help = "Disable all debugging features such as prints, logging runtime errors, and logging api return codes", long = "release", action = ArgAction::SetTrue)]
#[serde(default)]
pub release: bool,
}
impl Default for DebugFeatures {
fn default() -> Self {
DebugFeatures {
log_runtime_errors: true,
log_prints: true,
generate_debug_info: false,
release: false,
}
}
}
#[derive(Args, Deserialize, Default, Debug, PartialEq)]
pub struct Optimizations {
#[arg(name = "DEADSTORAGE", help = "Disable dead storage codegen optimization", long = "no-dead-storage", action = ArgAction::SetFalse, display_order = 3)]
#[serde(default = "default_true", rename(deserialize = "dead-storage"))]
pub dead_storage: bool,
#[arg(name = "CONSTANTFOLDING", help = "Disable constant folding codegen optimization", long = "no-constant-folding", action = ArgAction::SetFalse, display_order = 1)]
#[serde(default = "default_true", rename(deserialize = "constant-folding"))]
pub constant_folding: bool,
#[arg(name = "STRENGTHREDUCE", help = "Disable strength reduce codegen optimization", long = "no-strength-reduce", action = ArgAction::SetFalse, display_order = 2)]
#[serde(default = "default_true", rename(deserialize = "strength-reduce"))]
pub strength_reduce: bool,
#[arg(name = "VECTORTOSLICE", help = "Disable vector to slice codegen optimization", long = "no-vector-to-slice", action = ArgAction::SetFalse, display_order = 4)]
#[serde(default = "default_true", rename(deserialize = "vector-to-slice"))]
pub vector_to_slice: bool,
#[arg(name = "COMMONSUBEXPRESSIONELIMINATION", help = "Disable common subexpression elimination", long = "no-cse", action = ArgAction::SetFalse, display_order = 5)]
#[serde(
default = "default_true",
rename(deserialize = "common-subexpression-elimination")
)]
pub common_subexpression_elimination: bool,
#[arg(name = "OPT", help = "Set llvm optimizer level ", short = 'O', default_value = "default", value_parser = ["none", "less", "default", "aggressive"], num_args = 1)]
#[serde(rename(deserialize = "llvm-IR-optimization-level"))]
pub opt_level: Option<String>,
#[cfg(feature = "wasm_opt")]
#[arg(
name = "WASM_OPT",
help = "wasm-opt passes for Wasm targets (0, 1, 2, 3, 4, s or z; see the wasm-opt help for more details)",
long = "wasm-opt",
num_args = 1
)]
#[serde(rename(deserialize = "wasm-opt"))]
pub wasm_opt_passes: Option<OptimizationPasses>,
}
pub trait TargetArgTrait {
fn get_name(&self) -> &String;
fn get_address_length(&self) -> &Option<u64>;
fn get_value_length(&self) -> &Option<u64>;
}
impl TargetArgTrait for TargetArg {
fn get_name(&self) -> &String {
&self.name
}
fn get_address_length(&self) -> &Option<u64> {
&self.address_length
}
fn get_value_length(&self) -> &Option<u64> {
&self.value_length
}
}
impl TargetArgTrait for CompileTargetArg {
fn get_name(&self) -> &String {
if let Some(name) = &self.name {
name
} else {
eprintln!("error: no target name specified");
exit(1);
}
}
fn get_address_length(&self) -> &Option<u64> {
&self.address_length
}
fn get_value_length(&self) -> &Option<u64> {
&self.value_length
}
}
pub(crate) fn target_arg<T: TargetArgTrait>(target_arg: &T) -> Target {
let target_name = target_arg.get_name();
if target_name == "solana" || target_name == "evm" {
if target_arg.get_address_length().is_some() {
eprintln!("error: address length cannot be modified except for polkadot target");
exit(1);
}
if target_arg.get_value_length().is_some() {
eprintln!("error: value length cannot be modified except for polkadot target");
exit(1);
}
}
let target = match target_name.as_str() {
"solana" => solang::Target::Solana,
"polkadot" => solang::Target::Polkadot {
address_length: target_arg.get_address_length().unwrap_or(32) as usize,
value_length: target_arg.get_value_length().unwrap_or(16) as usize,
},
"evm" => solang::Target::EVM,
"soroban" => solang::Target::Soroban,
_ => unreachable!(),
};
target
}
pub trait PackageTrait {
fn get_input(&self) -> &Vec<PathBuf>;
fn get_import_path(&self) -> &Option<Vec<PathBuf>>;
fn get_import_map(&self) -> &Option<Vec<(String, PathBuf)>>;
}
impl PackageTrait for CompilePackage {
fn get_input(&self) -> &Vec<PathBuf> {
if let Some(files) = &self.input {
files
} else {
eprintln!(
"No input files specified, please specifiy them in solang.toml or in command line"
);
exit(1);
}
}
fn get_import_path(&self) -> &Option<Vec<PathBuf>> {
&self.import_path
}
fn get_import_map(&self) -> &Option<Vec<(String, PathBuf)>> {
&self.import_map
}
}
impl PackageTrait for DocPackage {
fn get_input(&self) -> &Vec<PathBuf> {
&self.input
}
fn get_import_path(&self) -> &Option<Vec<PathBuf>> {
&self.import_path
}
fn get_import_map(&self) -> &Option<Vec<(String, PathBuf)>> {
&self.import_map
}
}
pub fn imports_arg<T: PackageTrait>(package: &T) -> FileResolver {
let mut resolver = FileResolver::default();
if let Some(paths) = package.get_import_path() {
let dups: Vec<_> = paths.iter().duplicates().collect();
if !dups.is_empty() {
eprintln!(
"error: import paths {} specifed more than once",
dups.iter().map(|p| format!("'{}'", p.display())).join(", ")
);
exit(1);
}
for path in paths {
resolver.add_import_path(path);
}
}
if let Some(maps) = package.get_import_map() {
for (map, path) in maps {
let os_map = OsString::from(map);
if let Some((_, existing_path)) = resolver
.get_import_paths()
.iter()
.find(|(m, _)| *m == Some(os_map.clone()))
{
eprintln!(
"warning: mapping '{}' to '{}' is overwritten",
map,
existing_path.display()
)
}
resolver.add_import_map(os_map, path.clone());
}
}
resolver
}
pub fn options_arg(
debug: &DebugFeatures,
optimizations: &Optimizations,
compiler_inputs: &CompilePackage,
) -> Options {
let opt_level = if let Some(level) = &optimizations.opt_level {
match level.as_str() {
"none" => OptimizationLevel::None,
"less" => OptimizationLevel::Less,
"default" => OptimizationLevel::Default,
"aggressive" => OptimizationLevel::Aggressive,
_ => unreachable!(),
}
} else {
OptimizationLevel::Default
};
Options {
dead_storage: optimizations.dead_storage,
constant_folding: optimizations.constant_folding,
strength_reduce: optimizations.strength_reduce,
vector_to_slice: optimizations.vector_to_slice,
common_subexpression_elimination: optimizations.common_subexpression_elimination,
generate_debug_information: debug.generate_debug_info,
opt_level,
log_runtime_errors: debug.log_runtime_errors && !debug.release,
log_prints: debug.log_prints && !debug.release,
#[cfg(feature = "wasm_opt")]
wasm_opt: optimizations.wasm_opt_passes.or(if debug.release {
Some(OptimizationPasses::Z)
} else {
None
}),
soroban_version: compiler_inputs.soroban_version,
}
}
fn parse_import_map(map: &str) -> Result<(String, PathBuf), String> {
if let Some((var, value)) = map.split_once('=') {
Ok((var.to_owned(), PathBuf::from(value)))
} else {
Err("contains no '='".to_owned())
}
}
fn parse_version(version: &str) -> Result<String, String> {
match Version::parse(version) {
Ok(version) => Ok(version.to_string()),
Err(err) => Err(err.to_string()),
}
}
fn deserialize_inline_table<'de, D>(
deserializer: D,
) -> Result<Option<Vec<(String, PathBuf)>>, D::Error>
where
D: serde::Deserializer<'de>,
{
let res: Option<toml::Table> = Option::deserialize(deserializer)?;
match res {
Some(table) => Ok(Some(
table
.iter()
.map(|f| {
(
f.0.clone(),
if f.1.is_str() {
PathBuf::from(f.1.as_str().unwrap())
} else {
let key = f.1;
eprintln!("error: invalid value for import map {key}");
exit(1)
},
)
})
.collect(),
)),
None => Ok(None),
}
}
fn deserialize_version<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
let res: Option<String> = Option::deserialize(deserializer)?;
match res {
Some(version) => match Version::parse(&version) {
Ok(version) => Ok(Some(version.to_string())),
Err(err) => Err(serde::de::Error::custom(err)),
},
None => Ok(None),
}
}
fn deserialize_emit<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
let str: Option<String> = Option::deserialize(deserializer)?;
match str {
Some(value) => {
match value.as_str() {
"ast-dot"|"cfg"|"llvm-ir"|"llvm-bc"|"object"|"asm" =>
Ok(Some(value))
,
_ => Err(serde::de::Error::custom("Invalid option for `emit`. Valid options are: `ast-dot`, `cfg`, `llvm-ir`, `llvm-bc`, `object`, `asm`"))
}
}
None => Ok(None),
}
}
fn default_true() -> bool {
true
}
fn explicit_args(matches: &ArgMatches) -> Vec<&Id> {
matches
.ids()
.filter(|x| {
matches!(
matches.value_source(x.as_str()),
Some(ValueSource::CommandLine)
)
})
.collect()
}