use std::{
io::BufReader,
path::PathBuf,
process::{Command, Stdio},
};
use camino::Utf8PathBuf;
use cargo_metadata::CompilerMessage;
use derivative::Derivative;
use tracing::instrument;
use crate::{
handle_compiler_msg, BuildError, ExecutableArtifact, FeatureSpec, PackageSpec, MSG_FORMAT,
};
#[derive(Derivative)]
#[derivative(Debug)]
pub struct Compiler {
workspace: Option<PathBuf>,
package: PackageSpec,
name: String,
is_example: bool,
#[derivative(Debug = "ignore")]
on_compiler_msg: Option<Box<dyn FnMut(CompilerMessage)>>,
target_dir: Option<Utf8PathBuf>,
features: Option<FeatureSpec>,
is_release: bool,
}
impl Compiler {
#[must_use]
pub fn bin(name: impl Into<String>) -> Self {
Self::new(name, false)
}
#[must_use]
pub fn example(name: impl Into<String>) -> Self {
Self::new(name, true)
}
fn new(name: impl Into<String>, is_example: bool) -> Self {
Self {
workspace: None,
package: PackageSpec::Any,
name: name.into(),
is_example,
on_compiler_msg: None,
target_dir: None,
features: None,
is_release: false,
}
}
pub fn workspace(&mut self, path: impl Into<PathBuf>) -> &mut Self {
self.workspace = Some(path.into());
self
}
pub fn package(&mut self, package: PackageSpec) -> &mut Self {
self.package = package;
self
}
pub fn on_compiler_msg(&mut self, cb: impl FnMut(CompilerMessage) + 'static) -> &mut Self {
self.on_compiler_msg = Some(Box::new(cb));
self
}
pub fn target_dir(&mut self, target_dir: impl Into<Utf8PathBuf>) -> &mut Self {
self.target_dir = Some(target_dir.into());
self
}
pub fn features(&mut self, features: FeatureSpec) -> &mut Self {
self.features = Some(features);
self
}
pub fn release(&mut self, is_release: bool) -> &mut Self {
self.is_release = is_release;
self
}
#[instrument(err)]
pub fn compile(&mut self) -> Result<ExecutableArtifact, BuildError> {
let mut cmd = Command::new("cargo");
cmd.arg("build")
.arg(MSG_FORMAT)
.args(&["--package", self.package.as_repr()])
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.stdin(Stdio::null());
if let Some(features) = &self.features {
cmd.args(features.to_args());
}
if let Some(ref workspace) = self.workspace {
cmd.current_dir(workspace);
}
if self.is_release {
cmd.arg("--release");
}
if let Some(ref target_dir) = self.target_dir {
cmd.args(&["--target-dir", target_dir.as_str()]);
}
if self.is_example {
cmd.args(&["--example", &self.name]);
} else {
cmd.args(&["--bin", &self.name]);
}
let mut cmd = cmd.spawn()?;
let stdout = cmd.stdout.take().unwrap();
let stderr = cmd.stderr.take().unwrap();
let mut artifact = None;
let messages = cargo_metadata::Message::parse_stream(BufReader::new(stdout));
for msg in messages {
match msg? {
cargo_metadata::Message::CompilerMessage(msg) => {
handle_compiler_msg(msg, &mut self.on_compiler_msg)
}
cargo_metadata::Message::CompilerArtifact(art) => {
if art.executable.is_none() {
continue;
}
assert!(
artifact.is_none(),
"Expected cargo build with --bin or --example to only produce one executable"
);
artifact = Some(art);
}
_ => {}
}
}
if cmd.wait()?.success() {
let artifact = artifact
.expect("If cargo build exits with success should have built an executable");
Ok(ExecutableArtifact::maybe_from(artifact).expect("Artifact has executable"))
} else {
Err(BuildError::from_stderr(stderr))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_common::{init, Result};
use pretty_assertions::{assert_eq, assert_ne};
#[test]
fn test_features() -> Result {
let artifact = Compiler::bin("hello_world")
.workspace("samples/hello_world")
.features(FeatureSpec::new(vec!["non_default_feature".into()]))
.compile()?;
assert_eq!(
vec![
"default".to_string(),
"default_feature".to_string(),
"non_default_feature".to_string()
],
artifact.features
);
Ok(())
}
#[test]
fn test_no_default_features() -> Result {
let artifact = Compiler::bin("hello_world")
.workspace("samples/hello_world")
.features(FeatureSpec::none())
.compile()?;
assert!(artifact.features.is_empty());
Ok(())
}
#[test]
fn test_release_false() -> Result {
init();
let artifact = Compiler::bin("hello_world")
.workspace("samples/hello_world")
.release(false)
.compile()?;
assert_eq!("0", artifact.profile.opt_level);
Ok(())
}
#[test]
fn test_release_true() -> Result {
let artifact = Compiler::bin("hello_world")
.workspace("samples/hello_world")
.release(true)
.compile()?;
assert_ne!("0", artifact.profile.opt_level);
Ok(())
}
#[test]
fn test_release_default() -> Result {
init();
let artifact = Compiler::bin("hello_world")
.workspace("samples/hello_world")
.compile()?;
assert_eq!("0", artifact.profile.opt_level);
Ok(())
}
#[test]
fn test_cargo_error() {
init();
let result = Compiler::bin("hello_world").workspace("/").compile();
assert!(matches!(
result,
Err(BuildError::Cargo(stderr)) if stderr == "error: could not find `Cargo.toml` in `/` or any parent directory\n"
));
}
#[test]
fn test_bin_main() -> Result {
init();
let artifact = Compiler::bin("hello_world")
.workspace("samples/hello_world")
.compile()?;
assert_eq!("hello_world", artifact.target.name);
assert!(artifact.target.src_path.ends_with("src/main.rs"));
Ok(())
}
#[test]
fn test_bin_2() -> Result {
init();
let artifact = Compiler::bin("bin_2")
.workspace("samples/hello_world")
.compile()?;
assert_eq!("bin_2", artifact.target.name);
assert!(artifact.target.src_path.ends_with("src/bin/bin_2.rs"));
Ok(())
}
#[test]
fn test_bin_nonexistent() {
let result = Compiler::bin("bin_that_doesnt_exist")
.workspace("samples/hello_world")
.compile();
assert!(matches!(result, Err(BuildError::NotFound(_))));
}
#[test]
fn test_bin_nonexistent_package() {
init();
let result = Compiler::bin("bin_that_doesnt_exist")
.package(PackageSpec::name("package_that_doesnt_exist"))
.workspace("samples/hello_world")
.compile();
assert!(matches!(result, Err(BuildError::PackageNotFound(_))));
}
#[test]
fn test_example() -> Result {
init();
let artifact = Compiler::example("example_1")
.workspace("samples/hello_world")
.compile()?;
assert_eq!("example_1", artifact.target.name);
assert!(artifact.target.src_path.ends_with("examples/example_1.rs"));
Ok(())
}
#[test]
fn test_example_nonexistent() {
init();
let result = Compiler::example("example_does_not_exist")
.workspace("samples/hello_world")
.compile();
assert!(matches!(result, Err(BuildError::NotFound(_))));
}
#[test]
fn test_example_nonexistent_package() {
init();
let result = Compiler::example("example_1")
.workspace("samples/hello_world")
.package(PackageSpec::name("nonexistent_package"))
.compile();
assert!(matches!(result, Err(BuildError::PackageNotFound(_))));
}
#[test]
fn test_ws_member_main() -> Result {
init();
init();
let artifact = Compiler::bin("ws_member")
.package(PackageSpec::name("ws_member"))
.workspace("samples/hello_world")
.compile()?;
assert_eq!("ws_member", artifact.target.name);
assert!(artifact.target.src_path.ends_with("ws_member/src/main.rs"));
Ok(())
}
}