1use std::ffi::OsStr;
2use std::fs;
3use std::io::{BufRead, BufReader};
4use std::process::Command;
5use std::{
6 collections::{BTreeMap, HashSet},
7 path::PathBuf,
8};
9use std::{env, thread};
10
11use camino::{Utf8Path, Utf8PathBuf};
12use cargo_metadata::{Artifact, Message};
13use color_eyre::eyre::{ContextCompat, WrapErr};
14use log::{error, info};
15
16use crate::common::ColorPreference;
17use crate::types::manifest::CargoManifestPath;
18
19mod print;
20pub(crate) use print::*;
21
22pub(crate) const fn dylib_extension() -> &'static str {
23 #[cfg(target_os = "linux")]
24 return "so";
25
26 #[cfg(target_os = "macos")]
27 return "dylib";
28
29 #[cfg(target_os = "windows")]
30 return "dll";
31
32 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
33 compile_error!("Unsupported platform");
34}
35
36fn invoke_cargo<A, P, E, S, EK, EV>(
42 command: &str,
43 args: A,
44 working_dir: Option<P>,
45 env: E,
46 color: ColorPreference,
47) -> color_eyre::eyre::Result<Vec<Artifact>>
48where
49 A: IntoIterator<Item = S>,
50 P: AsRef<Utf8Path>,
51 E: IntoIterator<Item = (EK, EV)>,
52 S: AsRef<OsStr>,
53 EK: AsRef<OsStr>,
54 EV: AsRef<OsStr>,
55{
56 let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
57 let mut cmd = Command::new(cargo);
58
59 cmd.envs(env);
60
61 if let Some(path) = working_dir {
62 let path = force_canonicalize_dir(path.as_ref())?;
63 log::debug!("Setting cargo working dir to '{}'", path);
64 cmd.current_dir(path);
65 }
66
67 cmd.arg(command);
68 cmd.args(args);
69
70 match color {
71 ColorPreference::Auto => cmd.args(["--color", "auto"]),
72 ColorPreference::Always => cmd.args(["--color", "always"]),
73 ColorPreference::Never => cmd.args(["--color", "never"]),
74 };
75
76 log::info!("Invoking cargo: {:?}", cmd);
77
78 let mut child = cmd
79 .stdout(std::process::Stdio::piped())
81 .stderr(std::process::Stdio::piped())
82 .spawn()
83 .wrap_err_with(|| format!("Error executing `{:?}`", cmd))?;
84 let child_stdout = child
85 .stdout
86 .take()
87 .wrap_err("could not attach to child stdout")?;
88 let child_stderr = child
89 .stderr
90 .take()
91 .wrap_err("could not attach to child stderr")?;
92
93 let thread_stdout = thread::spawn(move || -> color_eyre::eyre::Result<_, std::io::Error> {
95 let mut artifacts = vec![];
96 let stdout_reader = std::io::BufReader::new(child_stdout);
97 for message in Message::parse_stream(stdout_reader) {
98 match message? {
99 Message::CompilerArtifact(artifact) => {
100 artifacts.push(artifact);
101 }
102 Message::CompilerMessage(message) => {
103 if let Some(msg) = message.message.rendered {
104 for line in msg.lines() {
105 eprintln!(" │ {}", line);
106 }
107 }
108 }
109 _ => {}
110 };
111 }
112
113 Ok(artifacts)
114 });
115 let thread_stderr = thread::spawn(move || {
116 let stderr_reader = BufReader::new(child_stderr);
117 let stderr_lines = stderr_reader.lines();
118 for line in stderr_lines {
119 eprintln!(" │ {}", line.expect("failed to read cargo stderr"));
120 }
121 });
122
123 let result = thread_stdout.join().expect("failed to join stdout thread");
124 thread_stderr.join().expect("failed to join stderr thread");
125
126 let output = child.wait()?;
127
128 if output.success() {
129 Ok(result?)
130 } else {
131 color_eyre::eyre::bail!("`{:?}` failed with exit code: {:?}", cmd, output.code());
132 }
133}
134
135pub(crate) fn invoke_rustup<I, S>(args: I) -> color_eyre::eyre::Result<Vec<u8>>
136where
137 I: IntoIterator<Item = S>,
138 S: AsRef<OsStr>,
139{
140 let rustup = env::var("RUSTUP").unwrap_or_else(|_| "rustup".to_string());
141
142 let mut cmd = Command::new(rustup);
143 cmd.args(args);
144
145 log::info!("Invoking rustup: {:?}", cmd);
146
147 let child = cmd
148 .stdout(std::process::Stdio::piped())
149 .spawn()
150 .wrap_err_with(|| format!("Error executing `{:?}`", cmd))?;
151
152 let output = child.wait_with_output()?;
153 if output.status.success() {
154 Ok(output.stdout)
155 } else {
156 color_eyre::eyre::bail!(
157 "`{:?}` failed with exit code: {:?}",
158 cmd,
159 output.status.code()
160 );
161 }
162}
163
164pub struct CompilationArtifact {
165 pub path: Utf8PathBuf,
166 pub fresh: bool,
167}
168
169pub(crate) fn compile_project(
171 manifest_path: &CargoManifestPath,
172 args: &[&str],
173 mut env: Vec<(&str, &str)>,
174 artifact_extension: &str,
175 hide_warnings: bool,
176 color: ColorPreference,
177) -> color_eyre::eyre::Result<CompilationArtifact> {
178 let mut final_env = BTreeMap::new();
179
180 if hide_warnings {
181 env.push(("RUSTFLAGS", "-Awarnings"));
182 }
183
184 for (key, value) in env {
185 match key {
186 "RUSTFLAGS" => {
187 let rustflags: &mut String = final_env
188 .entry(key)
189 .or_insert_with(|| std::env::var(key).unwrap_or_default());
190 if !rustflags.is_empty() {
191 rustflags.push(' ');
192 }
193 rustflags.push_str(value);
194 }
195 _ => {
196 final_env.insert(key, value.to_string());
197 }
198 }
199 }
200
201 let artifacts = invoke_cargo(
202 "build",
203 [&["--message-format=json-render-diagnostics"], args].concat(),
204 manifest_path.directory().ok(),
205 final_env.iter(),
206 color,
207 )?;
208
209 let compile_artifact = artifacts.last().wrap_err(
212 "Cargo failed to produce any compilation artifacts. \
213 Please check that your project contains a Utilit smart contract.",
214 )?;
215 let dylib_files = compile_artifact
218 .filenames
219 .iter()
220 .filter(|f| {
221 f.extension()
222 .map(|e| e == artifact_extension)
223 .unwrap_or(false)
224 })
225 .cloned()
226 .collect();
227 let mut dylib_files_iter = Vec::into_iter(dylib_files);
228 match (dylib_files_iter.next(), dylib_files_iter.next()) {
229 (None, None) => color_eyre::eyre::bail!(
230 "Compilation resulted in no '.{artifact_extension}' target files. \
231 Please check that your project contains a Utility smart contract."
232 ),
233 (Some(path), None) => Ok(CompilationArtifact {
234 path,
235 fresh: !compile_artifact.fresh,
236 }),
237 _ => color_eyre::eyre::bail!(
238 "Compilation resulted in more than one '.{}' target file: {:?}",
239 artifact_extension,
240 dylib_files_iter.as_slice()
241 ),
242 }
243}
244
245pub(crate) fn force_canonicalize_dir(dir: &Utf8Path) -> color_eyre::eyre::Result<Utf8PathBuf> {
247 fs::create_dir_all(dir).wrap_err_with(|| format!("failed to create directory `{}`", dir))?;
248 Utf8PathBuf::from_path_buf(
252 dunce::canonicalize(dir)
253 .wrap_err_with(|| format!("failed to canonicalize path: {} ", dir))?,
254 )
255 .map_err(|err| color_eyre::eyre::eyre!("failed to convert path {}", err.to_string_lossy()))
256}
257
258pub(crate) fn copy(from: &Utf8Path, to: &Utf8Path) -> color_eyre::eyre::Result<Utf8PathBuf> {
262 let out_path = to.join(from.file_name().unwrap());
263 if from != out_path {
264 fs::copy(from, &out_path)
265 .wrap_err_with(|| format!("failed to copy `{}` to `{}`", from, out_path))?;
266 }
267 Ok(out_path)
268}
269
270pub(crate) fn extract_abi_entries(
271 dylib_path: &Utf8Path,
272) -> color_eyre::eyre::Result<Vec<unc_abi::__private::ChunkedAbiEntry>> {
273 let dylib_file_contents = fs::read(dylib_path)?;
274 let object = symbolic_debuginfo::Object::parse(&dylib_file_contents)?;
275 log::debug!(
276 "A dylib was built at {:?} with format {} for architecture {}",
277 &dylib_path,
278 &object.file_format(),
279 &object.arch()
280 );
281 let unc_abi_symbols = object
282 .symbols()
283 .flat_map(|sym| sym.name)
284 .filter(|sym_name| sym_name.starts_with("__unc_abi_"))
285 .collect::<HashSet<_>>();
286 if unc_abi_symbols.is_empty() {
287 color_eyre::eyre::bail!("No Utility ABI symbols found in the dylib");
288 }
289 log::debug!("Detected Utility ABI symbols: {:?}", &unc_abi_symbols);
290
291 let mut entries = vec![];
292 unsafe {
293 let lib = libloading::Library::new(dylib_path)?;
294 for symbol in unc_abi_symbols {
295 let entry: libloading::Symbol<extern "C" fn() -> (*const u8, usize)> =
296 lib.get(symbol.as_bytes())?;
297 let (ptr, len) = entry();
298 let data = Vec::from_raw_parts(ptr as *mut _, len, len);
299 match serde_json::from_slice(&data) {
300 Ok(entry) => entries.push(entry),
301 Err(err) => {
302 let mut err_str = err.to_string();
304 if let Some((msg, rest)) = err_str.rsplit_once(" at line ") {
305 if let Some((line, col)) = rest.rsplit_once(" column ") {
306 if line.chars().all(|c| c.is_numeric())
307 && col.chars().all(|c| c.is_numeric())
308 {
309 err_str.truncate(msg.len());
310 err_str.shrink_to_fit();
311 color_eyre::eyre::bail!(err_str);
312 }
313 }
314 }
315 color_eyre::eyre::bail!(err);
316 }
317 };
318 }
319 }
320 Ok(entries)
321}
322
323pub(crate) const COMPILATION_TARGET: &str = "wasm32-unknown-unknown";
324
325fn get_rustc_wasm32_unknown_unknown_target_libdir() -> color_eyre::eyre::Result<PathBuf> {
326 let command = Command::new("rustc")
327 .args(["--target", COMPILATION_TARGET, "--print", "target-libdir"])
328 .output()?;
329
330 if command.status.success() {
331 Ok(String::from_utf8(command.stdout)?.trim().into())
332 } else {
333 color_eyre::eyre::bail!(
334 "Getting rustc's wasm32-unknown-unknown target wasn't successful. Got {}",
335 command.status,
336 )
337 }
338}
339
340pub fn wasm32_target_libdir_exists() -> bool {
341 let result = get_rustc_wasm32_unknown_unknown_target_libdir();
342
343 match result {
344 Ok(wasm32_target_libdir_path) => {
345 if wasm32_target_libdir_path.exists() {
346 info!(
347 "Found {COMPILATION_TARGET} in {:?}",
348 wasm32_target_libdir_path
349 );
350 true
351 } else {
352 info!(
353 "Failed to find {COMPILATION_TARGET} in {:?}",
354 wasm32_target_libdir_path
355 );
356 false
357 }
358 }
359 Err(_) => {
360 error!("Some error in getting the target libdir, trying rustup..");
361
362 invoke_rustup(["target", "list", "--installed"])
363 .map(|stdout| {
364 stdout
365 .lines()
366 .any(|target| target.as_ref().map_or(false, |t| t == COMPILATION_TARGET))
367 })
368 .is_ok()
369 }
370 }
371}