diff --git a/Cargo.lock b/Cargo.lock index 0a7b47153..aeeaa445a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -814,6 +814,7 @@ dependencies = [ "clap", "colored", "contract-metadata", + "duct", "heck", "hex", "impl-serde", @@ -826,6 +827,7 @@ dependencies = [ "serde_json", "strum", "tempfile", + "term_size", "toml 0.7.3", "tracing", "url", @@ -1171,6 +1173,18 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +[[package]] +name = "duct" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ae3fc31835f74c2a7ceda3aeede378b0ae2e74c8f1c36559fcc9ae2a4e7d3e" +dependencies = [ + "libc", + "once_cell", + "os_pipe", + "shared_child", +] + [[package]] name = "dyn-clonable" version = "0.9.0" @@ -2681,6 +2695,16 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "os_pipe" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae859aa07428ca9a929b936690f8b12dc5f11dd8c6992a18ca93919f28bc177" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "output_vt100" version = "0.1.3" @@ -3719,6 +3743,16 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shared_child" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "signature" version = "1.6.4" @@ -4352,6 +4386,16 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "term_size" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "termcolor" version = "1.2.0" diff --git a/crates/build/Cargo.toml b/crates/build/Cargo.toml index e9199b3a2..919348e2c 100644 --- a/crates/build/Cargo.toml +++ b/crates/build/Cargo.toml @@ -19,6 +19,7 @@ blake2 = "0.10.6" cargo_metadata = "0.15.4" colored = "2.0.0" clap = { version = "4.2.7", features = ["derive", "env"] } +duct = "0.13.6" heck = "0.4.0" hex = "0.4.3" impl-serde = "0.4.0" @@ -31,6 +32,7 @@ semver = { version = "1.0.17", features = ["serde"] } serde = { version = "1", default-features = false, features = ["derive"] } serde_json = "1.0.96" tempfile = "3.5.0" +term_size = "0.3.2" url = { version = "2.3.1", features = ["serde"] } wasm-opt = "0.112.0" which = "4.4.0" diff --git a/crates/build/src/lib.rs b/crates/build/src/lib.rs index 237eeb325..010e11483 100644 --- a/crates/build/src/lib.rs +++ b/crates/build/src/lib.rs @@ -83,7 +83,9 @@ use parity_wasm::elements::{ }; use semver::Version; use std::{ + collections::VecDeque, fs, + io, path::{ Path, PathBuf, @@ -291,9 +293,10 @@ fn exec_cargo_for_onchain_target( None }; - util::invoke_cargo(command, &args, manifest_path.directory(), verbosity, env)?; + let cargo = + util::cargo_cmd(command, &args, manifest_path.directory(), verbosity, env); - Ok(()) + invoke_cargo_and_scan_for_error(cargo) }; if unstable_flags.original_manifest { @@ -320,6 +323,51 @@ fn exec_cargo_for_onchain_target( Ok(()) } +/// Executes the supplied cargo command, reading the output and scanning for known errors. +/// Writes the captured stderr back to stderr and maintains the cargo tty progress bar. +fn invoke_cargo_and_scan_for_error(cargo: duct::Expression) -> Result<()> { + macro_rules! eprintln_red { + ($value:expr) => {{ + use colored::Colorize as _; + ::std::eprintln!("{}", $value.bright_red().bold()); + }}; + } + + let cargo = util::cargo_tty_output(cargo); + + let missing_main_err = "error[E0601]".as_bytes(); + let mut err_buf = VecDeque::with_capacity(missing_main_err.len()); + + let mut reader = cargo.stderr_to_stdout().reader()?; + let mut buffer = [0u8; 1]; + + loop { + let bytes_read = io::Read::read(&mut reader, &mut buffer)?; + for byte in buffer[0..bytes_read].iter() { + err_buf.push_back(*byte); + if err_buf.len() > missing_main_err.len() { + let byte = err_buf.pop_front().expect("buffer is not empty"); + io::Write::write(&mut io::stderr(), &[byte])?; + } + } + if missing_main_err == err_buf.make_contiguous() { + eprintln_red!("\nExited with error: [E0601]"); + eprintln_red!( + "Your contract must be annotated with the `no_main` attribute.\n" + ); + eprintln_red!("Examples how to do this:"); + eprintln_red!(" - `#![cfg_attr(not(feature = \"std\"), no_std, no_main)]`"); + eprintln_red!(" - `#[no_main]`\n"); + return Err(anyhow::anyhow!("missing `no_main` attribute")) + } + if bytes_read == 0 { + break + } + buffer = [0u8; 1]; + } + Ok(()) +} + /// Executes `cargo dylint` with the ink! linting driver that is built during /// the `build.rs`. /// @@ -357,8 +405,15 @@ fn exec_cargo_dylint(crate_metadata: &CrateMetadata, verbosity: Verbosity) -> Re Ok(()) })? .using_temp(|manifest_path| { - util::invoke_cargo("dylint", &args, manifest_path.directory(), verbosity, env) - .map(|_| ()) + let cargo = util::cargo_cmd( + "dylint", + &args, + manifest_path.directory(), + verbosity, + env, + ); + cargo.run()?; + Ok(()) })?; Ok(()) @@ -548,7 +603,11 @@ fn assert_compatible_ink_dependencies( ) -> Result<()> { for dependency in ["parity-scale-codec", "scale-info"].iter() { let args = ["-i", dependency, "--duplicates"]; - let _ = util::invoke_cargo("tree", args, manifest_path.directory(), verbosity, vec![]) + let cargo = + util::cargo_cmd("tree", args, manifest_path.directory(), verbosity, vec![]); + cargo + .stdout_null() + .run() .with_context(|| { format!( "Mismatching versions of `{dependency}` were found!\n\ diff --git a/crates/build/src/metadata.rs b/crates/build/src/metadata.rs index f929b75d3..d6cad3fa2 100644 --- a/crates/build/src/metadata.rs +++ b/crates/build/src/metadata.rs @@ -149,16 +149,17 @@ pub(crate) fn execute( network.append_to_args(&mut args); features.append_to_args(&mut args); - let stdout = util::invoke_cargo( + let cmd = util::cargo_cmd( "run", args, crate_metadata.manifest_path.directory(), verbosity, vec![], - )?; + ); + let output = cmd.stdout_capture().run()?; let ink_meta: serde_json::Map = - serde_json::from_slice(&stdout)?; + serde_json::from_slice(&output.stdout)?; let metadata = ContractMetadata::new(source, contract, user, ink_meta); { let mut metadata = metadata.clone(); diff --git a/crates/build/src/util/mod.rs b/crates/build/src/util/mod.rs index c4273f3bb..0cc5a6a88 100644 --- a/crates/build/src/util/mod.rs +++ b/crates/build/src/util/mod.rs @@ -18,15 +18,13 @@ pub mod tests; use crate::Verbosity; -use anyhow::{ - Context, - Result, -}; +use anyhow::Result; +use duct::Expression; use std::{ - ffi::OsStr, + ffi::OsString, path::Path, - process::Command, }; +use term_size as _; // Returns the current Rust toolchain formatted by `-`. pub(crate) fn rust_toolchain() -> Result { @@ -36,7 +34,7 @@ pub(crate) fn rust_toolchain() -> Result { Ok(toolchain) } -/// Invokes `cargo` with the subcommand `command` and the supplied `args`. +/// Builds an [`Expression`] for invoking `cargo`. /// /// In case `working_dir` is set, the command will be invoked with that folder /// as the working directory. @@ -44,67 +42,69 @@ pub(crate) fn rust_toolchain() -> Result { /// In case `env` is given environment variables can be either set or unset: /// * To _set_ push an item a la `("VAR_NAME", Some("VAR_VALUE"))` to the `env` vector. /// * To _unset_ push an item a la `("VAR_NAME", None)` to the `env` vector. -/// -/// If successful, returns the stdout bytes. -pub fn invoke_cargo( +pub fn cargo_cmd( command: &str, args: I, working_dir: Option

, verbosity: Verbosity, env: Vec<(&str, Option)>, -) -> Result> +) -> Expression where I: IntoIterator + std::fmt::Debug, - S: AsRef, + S: Into, P: AsRef, { let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); - let mut cmd = Command::new(cargo); + let mut cmd_args = Vec::new(); + + cmd_args.push(command); + cmd_args.push("--color=always"); + + match verbosity { + Verbosity::Quiet => cmd_args.push("--quiet"), + Verbosity::Verbose => { + if command != "dylint" { + cmd_args.push("--verbose") + } + } + Verbosity::Default => (), + }; + + let mut cmd_args: Vec = cmd_args.iter().map(Into::into).collect(); + for arg in args { + cmd_args.push(arg.into()); + } + + let mut cmd = duct::cmd(cargo, &cmd_args); env.iter().for_each(|(env_key, maybe_env_val)| { match maybe_env_val { - Some(env_val) => cmd.env(env_key, env_val), - None => cmd.env_remove(env_key), + Some(env_val) => cmd = cmd.env(env_key, env_val), + None => cmd = cmd.env_remove(env_key), }; }); if let Some(path) = working_dir { tracing::debug!("Setting cargo working dir to '{}'", path.as_ref().display()); - cmd.current_dir(path); + cmd = cmd.dir(path.as_ref()); } - cmd.arg(command); - cmd.args(args); - match verbosity { - Verbosity::Quiet => cmd.arg("--quiet"), - Verbosity::Verbose => { - if command != "dylint" { - cmd.arg("--verbose") - } else { - &mut cmd - } - } - Verbosity::Default => &mut cmd, - }; + cmd +} - tracing::debug!("Invoking cargo: {:?}", cmd); - - let child = cmd - // capture the stdout to return from this function as bytes - .stdout(std::process::Stdio::piped()) - .spawn() - .context(format!("Error executing `{cmd:?}`"))?; - let output = child.wait_with_output()?; - - if output.status.success() { - Ok(output.stdout) - } else { - anyhow::bail!( - "`{:?}` failed with exit code: {:?}", - cmd, - output.status.code() - ); - } +/// Configures the cargo command to output colour and the progress bar. +pub fn cargo_tty_output(cmd: Expression) -> Expression { + #[cfg(windows)] + let term_size = "100"; + + #[cfg(not(windows))] + let term_size = term_size::dimensions_stderr() + .map(|(width, _)| width.to_string()) + .unwrap_or_else(|| "100".to_string()); + + cmd.env("CARGO_TERM_COLOR", "always") + .env("CARGO_TERM_PROGRESS_WIDTH", term_size) + .env("CARGO_TERM_PROGRESS_WHEN", "always") } /// Returns the base name of the path.