neo_solidity/cli/cli_parts/cli_run/
single_file.rs1fn run_single_file(matches: &clap::ArgMatches) {
2 fn sanitize_stem_or_fallback(stem: Option<&str>, fallback: String) -> String {
3 stem.and_then(standard_json::sanitize_contract_name)
4 .unwrap_or(fallback)
5 }
6
7 fn is_output_directory(path: &str, assume_dir_when_missing: bool) -> bool {
8 fn looks_like_output_file(path: &str) -> bool {
9 path.ends_with(".nef")
10 || path.ends_with(".manifest.json")
11 || path.ends_with(".json")
12 || path.ends_with(".asm")
13 }
14
15 if path.ends_with('/') || path.ends_with('\\') {
16 return true;
17 }
18 let output_path = Path::new(path);
19 if output_path.is_dir() {
20 return true;
21 }
22 assume_dir_when_missing && !looks_like_output_file(path)
23 }
24
25 let sources: Vec<String> = matches
26 .get_many::<String>("source")
27 .map(|vals| vals.map(|s| s.to_string()).collect())
28 .unwrap_or_default();
29
30 if sources.is_empty() {
31 eprintln!("error: no input files provided");
32 std::process::exit(1);
33 }
34
35 let output_arg = matches.get_one::<String>("output").cloned();
36
37 let format = matches
38 .get_one::<String>("format")
39 .map(|s| s.as_str())
40 .unwrap_or("complete");
41 let optimizer_level = matches
42 .get_one::<String>("optimize")
43 .and_then(|s| s.parse::<u8>().ok())
44 .unwrap_or(2)
45 .min(3);
46 let verbose = matches.get_flag("verbose");
47 let nef_source_override = matches.get_one::<String>("nef-source").map(|s| s.as_str());
48 let deployer = matches.get_one::<String>("deployer").map(|s| s.as_str());
49 let include_paths: Vec<std::path::PathBuf> = matches
50 .get_many::<String>("include-path")
51 .map(|vals| vals.map(std::path::PathBuf::from).collect())
52 .unwrap_or_default();
53 let contract_filters: Vec<String> = matches
54 .get_many::<String>("contract")
55 .map(|vals| vals.map(|s| s.to_string()).collect())
56 .unwrap_or_default();
57 let use_callt = matches.get_flag("callt");
58 let deny_wildcard_permissions = matches.get_flag("deny-wildcard-permissions");
59 let deny_wildcard_contracts = matches.get_flag("deny-wildcard-contracts");
60 let deny_wildcard_methods = matches.get_flag("deny-wildcard-methods");
61 let manifest_permissions_file = matches.get_one::<String>("manifest-permissions").map(|s| s.as_str());
62 let manifest_permissions_mode = matches
63 .get_one::<String>("manifest-permissions-mode")
64 .map(|s| s.as_str())
65 .unwrap_or("merge");
66 let json_errors = matches.get_flag("json-errors");
67 let json_warnings = matches.get_flag("json-warnings");
68 let warn_suppress: Vec<String> = matches
69 .get_many::<String>("Wno")
70 .map(|vals| vals.map(|s| s.to_string()).collect())
71 .unwrap_or_default();
72 let warn_promote: Vec<String> = matches
73 .get_many::<String>("Werror")
74 .map(|vals| vals.map(|s| s.to_string()).collect())
75 .unwrap_or_default();
76
77 let manifest_permissions = match manifest_permissions_file {
78 Some(path) => match load_manifest_permissions_override(path, manifest_permissions_mode) {
79 Ok(override_permissions) => Some(override_permissions),
80 Err(err) => {
81 eprintln!("error: {err}");
82 std::process::exit(1);
83 }
84 },
85 None => None,
86 };
87
88 let file_ids: Vec<String> = if sources.len() > 1 {
89 use std::collections::HashMap;
90 use std::path::Component;
91
92 fn file_identity(path: &Path, fallback: String) -> String {
93 let mut parts: Vec<String> = Vec::new();
94 for component in path.components() {
95 match component {
96 Component::Normal(os) => {
97 parts.push(os.to_string_lossy().to_string());
98 }
99 Component::CurDir | Component::ParentDir => {}
100 Component::Prefix(_) | Component::RootDir => {}
101 }
102 }
103
104 if parts.is_empty() {
105 return fallback;
106 }
107
108 if let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) {
109 if let Some(last) = parts.last_mut() {
110 *last = stem.to_string();
111 }
112 }
113
114 let joined = parts.join("_");
115 standard_json::sanitize_contract_name(&joined).unwrap_or(fallback)
116 }
117
118 let mut stem_counts: HashMap<String, usize> = HashMap::new();
119 let stems: Vec<String> = sources
120 .iter()
121 .enumerate()
122 .map(|(file_index, input_file)| {
123 let file_stem = Path::new(input_file)
124 .file_stem()
125 .and_then(|stem| stem.to_str());
126 let sanitized =
127 sanitize_stem_or_fallback(file_stem, format!("contract{file_index}"));
128 *stem_counts.entry(sanitized.clone()).or_insert(0) += 1;
129 sanitized
130 })
131 .collect();
132
133 sources
134 .iter()
135 .enumerate()
136 .map(|(file_index, input_file)| {
137 let stem = stems[file_index].clone();
138 if stem_counts.get(&stem).copied().unwrap_or(0) <= 1 {
139 stem
140 } else {
141 file_identity(Path::new(input_file), format!("{stem}-{file_index}"))
142 }
143 })
144 .collect()
145 } else {
146 Vec::new()
147 };
148
149 if verbose {
150 println!("Neo Solidity Compiler v{}", env!("CARGO_PKG_VERSION"));
151 println!("Format: {}", format);
152 }
153
154 let deployer_le = match deployer {
155 Some(value) => match neo_solidity::neo::parse_uint160_hex_be(value) {
156 Ok(parsed) => Some(parsed),
157 Err(err) => {
158 eprintln!("error: invalid --deployer value: {err}");
159 std::process::exit(1);
160 }
161 },
162 None => None,
163 };
164
165 if sources.len() > 1 {
166 if let Some(output) = &output_arg {
167 if !is_output_directory(output, true) {
168 eprintln!(
169 "error: when compiling multiple input files, --output must be a directory (got '{output}')"
170 );
171 std::process::exit(1);
172 }
173 }
174
175 if verbose {
176 println!("Batch mode: {} input file(s)", sources.len());
177 if let Some(output) = &output_arg {
178 println!("Output directory: {}", output);
179 }
180 }
181 }
182
183 let mut emitted_any = false;
184
185 for (file_index, input_file) in sources.iter().enumerate() {
186 if sources.len() > 1 {
187 println!("(info) compiling {}", input_file);
188 }
189
190 let resolved = match resolve_solidity_sources_with_imports(
191 Path::new(input_file),
192 &include_paths,
193 ) {
194 Ok(resolved) => resolved,
195 Err(err) => {
196 eprintln!("Error resolving imports: {err}");
197 std::process::exit(1);
198 }
199 };
200 let input_content = resolved.combined_source;
201
202 if verbose {
203 println!(
204 "Resolved {} Solidity source file(s) ({} bytes combined)",
205 resolved.files.len(),
206 input_content.len()
207 );
208 }
209
210 let mut artifacts = compile_input_or_exit(
211 &input_content,
212 verbose,
213 CompileOptions {
214 optimizer_level,
215 use_callt,
216 deny_wildcard_permissions,
217 deny_wildcard_contracts,
218 deny_wildcard_methods,
219 manifest_permissions: manifest_permissions.clone(),
220 },
221 json_errors,
222 json_warnings,
223 );
224
225 let had_any_contracts = !artifacts.is_empty();
226
227 if !contract_filters.is_empty() {
228 artifacts.retain(|artifact| {
229 contract_filters
230 .iter()
231 .any(|name| name == &artifact.metadata.name)
232 });
233 }
234
235 if artifacts.is_empty() {
236 if !contract_filters.is_empty() && verbose {
237 println!(
238 "(info) no matching contract(s) found in {} for --contract {}",
239 input_file,
240 contract_filters.join(", ")
241 );
242 }
243 if verbose && !had_any_contracts {
244 eprintln!("warning: No contracts were found in {}", input_file);
245 }
246 continue;
247 }
248
249 let output_prefix = match (&output_arg, sources.len() > 1) {
250 (Some(output), true) => {
251 let output_dir = Path::new(output);
252 let stem_sanitized = file_ids
253 .get(file_index)
254 .cloned()
255 .unwrap_or_else(|| format!("contract{file_index}"));
256
257 if artifacts.len() == 1 {
258 let contract_name = artifacts[0].metadata.name.as_str();
259 let contract_sanitized =
260 sanitize_stem_or_fallback(Some(contract_name), stem_sanitized.clone());
261 let base = if contract_sanitized == stem_sanitized {
262 stem_sanitized
263 } else {
264 format!("{stem_sanitized}-{contract_sanitized}")
265 };
266 output_dir.join(base).to_string_lossy().to_string()
267 } else {
268 output_dir
269 .join(stem_sanitized)
270 .to_string_lossy()
271 .to_string()
272 }
273 }
274 (Some(output), false) => output.clone(),
275 (None, true) => file_ids
276 .get(file_index)
277 .cloned()
278 .unwrap_or_else(|| format!("contract{file_index}")),
279 (None, false) => Path::new(input_file)
280 .file_stem()
281 .and_then(|stem| stem.to_str())
282 .unwrap_or("contract")
283 .to_string(),
284 };
285
286 if verbose {
287 println!("Input: {}", input_file);
288 println!("Output prefix: {}", output_prefix);
289 }
290
291 if artifacts.len() > 1 {
292 println!(
293 "(info) detected {} contracts – outputs are suffixed with their contract names",
294 artifacts.len()
295 );
296 }
297
298 emit_contract_warnings(&artifacts, json_warnings, json_errors, &warn_suppress, &warn_promote);
299 let output_config = OutputConfig {
300 format,
301 output_prefix: &output_prefix,
302 input_file,
303 nef_source_override,
304 deployer: deployer_le,
305 json_errors,
306 json_warnings,
307 };
308 write_contract_outputs(&artifacts, &output_config);
309 emitted_any = true;
310 }
311
312 if !contract_filters.is_empty() && !emitted_any {
313 eprintln!(
314 "error: no matching contract(s) found for --contract {}",
315 contract_filters.join(", ")
316 );
317 std::process::exit(1);
318 }
319
320 println!("🎉 Neo Solidity compilation completed");
321}