use async_process::Command;
use cargo_metadata::{Error as MetadataError, Message, Metadata, MetadataCommand};
use tracing::debug;
use std::env;
use std::fmt::Debug;
use std::fs;
use std::io;
use std::path::Path;
use std::process::Stdio;
use crate::error::ErrorKind;
use crate::Result;
fn cargo_bin() -> Command {
match env::var_os("CARGO") {
Some(cargo) => Command::new(cargo),
None => Command::new("cargo"),
}
}
fn root_cargo_metadata() -> Result<Metadata> {
MetadataCommand::new().exec().map_err(|e| match e {
MetadataError::CargoMetadata { stderr } => ErrorKind::Io.message(stderr),
MetadataError::Io(err) => ErrorKind::Io.custom(err),
MetadataError::Utf8(err) => ErrorKind::DataConversion.custom(err),
MetadataError::ErrUtf8(err) => ErrorKind::DataConversion.custom(err),
MetadataError::Json(err) => ErrorKind::DataConversion.custom(err),
err @ MetadataError::NoJson => ErrorKind::DataConversion.message(err.to_string()),
})
}
async fn build_cargo_project<P: AsRef<Path> + Debug>(project_path: P) -> Result<Vec<Message>> {
let metadata = root_cargo_metadata()?;
let output = cargo_bin()
.args([
"build",
"--target",
"wasm32-unknown-unknown",
"--release",
"--message-format=json",
"--target-dir",
metadata.target_directory.as_str(),
])
.current_dir(&project_path)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await
.map_err(|e| ErrorKind::Io.custom(e))?;
debug!(
target: "workspaces",
"Building project '{:?}' resulted in status {:?}",
&project_path, output.status
);
if output.status.success() {
let reader = std::io::BufReader::new(output.stdout.as_slice());
Ok(Message::parse_stream(reader).map(|m| m.unwrap()).collect())
} else {
Err(ErrorKind::Io.message(format!(
"Failed to build project '{:?}'.\n\
Stderr:\n\
{}\n\
Stdout:\n\
{}",
project_path,
String::from_utf8(output.stderr).map_err(|e| ErrorKind::DataConversion.custom(e))?,
String::from_utf8(output.stdout).map_err(|e| ErrorKind::DataConversion.custom(e))?,
)))
}
}
pub async fn compile_project(project_path: &str) -> Result<Vec<u8>> {
let project_path = fs::canonicalize(project_path).map_err(|e| match e.kind() {
io::ErrorKind::NotFound => ErrorKind::Io.message(format!(
"Incorrect file supplied to compile_project('{}')",
project_path
)),
_ => ErrorKind::Io.custom(e),
})?;
let messages = build_cargo_project(project_path).await?;
let compile_artifact = messages
.iter()
.filter_map(|m| match m {
cargo_metadata::Message::CompilerArtifact(artifact) => Some(artifact),
_ => None,
})
.last()
.ok_or_else(|| {
ErrorKind::Io.message(
"Cargo failed to produce any compilation artifacts. \
Please check that your project contains a NEAR smart contract.",
)
})?;
let wasm_files = compile_artifact
.filenames
.iter()
.filter(|f| f.as_str().ends_with(".wasm"))
.collect::<Vec<_>>();
match wasm_files.as_slice() {
[] => Err(ErrorKind::Io.message(
"Compilation resulted in no '.wasm' target files. \
Please check that your project contains a NEAR smart contract.",
)),
[file] => {
let file = file.canonicalize().map_err(|e| ErrorKind::Io.custom(e))?;
Ok(tokio::fs::read(file)
.await
.map_err(|e| ErrorKind::Io.custom(e))?)
}
_ => Err(ErrorKind::Io.message(format!(
"Compilation resulted in more than one '.wasm' target file: {:?}",
wasm_files
))),
}
}