cargo_unc/util/
mod.rs

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
36/// Invokes `cargo` with the subcommand `command`, the supplied `args` and set `env` variables.
37///
38/// If `working_dir` is set, cargo process will be spawned in the specified directory.
39///
40/// Returns execution standard output as a byte array.
41fn 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        // capture the stdout to return from this function as bytes
80        .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    // stdout and stderr have to be processed concurrently to not block the process from progressing
94    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
169/// Builds the cargo project with manifest located at `manifest_path` and returns the path to the generated artifact.
170pub(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    // We find the last compiler artifact message which should contain information about the
210    // resulting dylib file
211    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    // The project could have generated many auxiliary files, we are only interested in
216    // dylib files with a specific (platform-dependent) extension
217    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
245/// Create the directory if it doesn't exist, and return the absolute path to it.
246pub(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    // use canonicalize from `dunce` create instead of default one from std because it's compatible with Windows UNC paths
249    // and don't break cargo compilation on Windows
250    // https://github.com/rust-lang/rust/issues/42869
251    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
258/// Copy a file to a destination.
259///
260/// Does nothing if the destination is the same as the source to avoid truncating the file.
261pub(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                    // unfortunately, we're unable to extract the raw error without Display-ing it first
303                    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}