use anyhow::Result;
use cargo_metadata::Message;
use probe_rs::InstructionSet;
use std::process::{Command, Stdio};
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ArtifactError {
#[error("Failed to canonicalize path '{work_dir}'.")]
Canonicalize {
#[source]
source: std::io::Error,
work_dir: String,
},
#[error("An IO error occurred during the execution of 'cargo build'.")]
Io(#[source] std::io::Error),
#[error("Failed to run cargo build: exit code = {0:?}.")]
CargoBuild(Option<i32>),
#[error("Multiple binary artifacts were found.")]
MultipleArtifacts,
#[error("No binary artifacts were found.")]
NoArtifacts,
}
pub struct Artifact {
path: PathBuf,
}
impl Artifact {
pub fn path(&self) -> &Path {
&self.path
}
}
pub fn build_artifact(work_dir: &Path, args: &[String]) -> Result<Artifact, ArtifactError> {
let work_dir = dunce::canonicalize(work_dir).map_err(|e| ArtifactError::Canonicalize {
source: e,
work_dir: format!("{}", work_dir.display()),
})?;
let cargo_executable = std::env::var("CARGO");
let cargo_executable = cargo_executable.as_deref().unwrap_or("cargo");
tracing::debug!(
"Running '{}' in directory {}",
cargo_executable,
work_dir.display()
);
let cargo_command = Command::new(cargo_executable)
.current_dir(work_dir)
.arg("build")
.args(args)
.args(["--message-format", "json-diagnostic-rendered-ansi"])
.stdout(Stdio::piped())
.spawn()
.map_err(ArtifactError::Io)?;
let output = cargo_command
.wait_with_output()
.map_err(ArtifactError::Io)?;
let messages = Message::parse_stream(&output.stdout[..]);
let mut target_artifact = None;
for message in messages {
match message.map_err(ArtifactError::Io)? {
Message::CompilerArtifact(artifact) => {
if artifact.executable.is_some() {
if target_artifact.is_some() {
return Err(ArtifactError::MultipleArtifacts);
}
target_artifact = Some(artifact);
}
}
Message::CompilerMessage(message) => {
if let Some(rendered) = message.message.rendered {
print!("{rendered}");
}
}
_ => (),
}
}
if !output.status.success() {
return Err(ArtifactError::CargoBuild(output.status.code()));
}
if let Some(artifact) = target_artifact {
Ok(Artifact {
path: PathBuf::from(artifact.executable.unwrap().as_path()),
})
} else {
Err(ArtifactError::NoArtifacts)
}
}
pub fn target_instruction_set(target: Option<&str>) -> Option<InstructionSet> {
cargo_target(target).and_then(|target| InstructionSet::from_target_triple(&target))
}
pub fn cargo_target(target: Option<&str>) -> Option<String> {
if let Some(target) = target {
return Some(target.to_string());
}
let cargo_config = cargo_config2::Config::load().ok()?;
cargo_config
.build
.target
.as_ref()
.and_then(|ts| Some(ts.first()?.triple().to_string()))
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn get_binary_artifact() {
let work_dir = test_project_dir("binary_project");
let mut expected_path = work_dir.join("target");
expected_path.push("debug");
expected_path.push(host_binary_name("binary_project"));
let args = [];
let binary_artifact =
build_artifact(&work_dir, &args).expect("Failed to read artifact path.");
assert_eq!(binary_artifact.path(), expected_path);
}
#[test]
fn get_binary_artifact_with_cargo_config() {
let work_dir = test_project_dir("binary_cargo_config");
let mut expected_path = work_dir.join("target");
expected_path.push("thumbv7m-none-eabi");
expected_path.push("debug");
expected_path.push("binary_cargo_config");
let args = [];
let binary_artifact =
build_artifact(&work_dir, &args).expect("Failed to read artifact path.");
assert_eq!(
binary_artifact.path(),
dunce::canonicalize(expected_path).expect("Failed to canonicalize path")
);
}
#[test]
fn get_binary_artifact_with_cargo_config_toml() {
let work_dir = test_project_dir("binary_cargo_config_toml");
let mut expected_path = work_dir.join("target");
expected_path.push("thumbv7m-none-eabi");
expected_path.push("debug");
expected_path.push("binary_cargo_config_toml");
let args = [];
let binary_artifact =
build_artifact(&work_dir, &args).expect("Failed to read artifact path.");
assert_eq!(
binary_artifact.path(),
dunce::canonicalize(expected_path).expect("Failed to canonicalize path")
);
}
#[test]
fn get_library_artifact_fails() {
let work_dir = test_project_dir("library_project");
let args = ["--release".to_owned()];
let binary_artifact = build_artifact(&work_dir, &args);
assert!(
binary_artifact.is_err(),
"Library project should not return a path to a binary, but got {}",
binary_artifact.unwrap().path().display()
);
}
#[test]
fn workspace_root() {
let work_dir = test_project_dir("workspace_project");
let mut expected_path = work_dir.join("target");
expected_path.push("release");
expected_path.push(host_binary_name("workspace_bin"));
let args = owned_args(&["--release"]);
let binary_artifact =
build_artifact(&work_dir, &args).expect("Failed to read artifact path.");
assert_eq!(binary_artifact.path(), expected_path);
}
#[test]
fn workspace_binary_package() {
let workspace_dir = test_project_dir("workspace_project");
let work_dir = workspace_dir.join("workspace_bin");
let mut expected_path = workspace_dir.join("target");
expected_path.push("release");
expected_path.push(host_binary_name("workspace_bin"));
let args = ["--release".to_owned()];
let binary_artifact =
build_artifact(&work_dir, &args).expect("Failed to read artifact path.");
assert_eq!(binary_artifact.path(), expected_path);
}
#[test]
fn workspace_library_package() {
let work_dir = test_project_dir("workspace_project/workspace_lib");
let args = ["--release".to_owned()];
let binary_artifact = build_artifact(&work_dir, &args);
assert!(
binary_artifact.is_err(),
"Library project should not return a path to a binary, but got {}",
binary_artifact.unwrap().path().display()
);
}
#[test]
fn multiple_binaries_in_crate() {
let work_dir = test_project_dir("multiple_binary_project");
let args = [];
let binary_artifact = build_artifact(&work_dir, &args);
assert!(
binary_artifact.is_err(),
"With multiple binaries, an error message should be shown. Got path '{}' instead.",
binary_artifact.unwrap().path().display()
);
}
#[test]
fn multiple_binaries_in_crate_select_binary() {
let work_dir = test_project_dir("multiple_binary_project");
let mut expected_path = work_dir.join("target");
expected_path.push("debug");
expected_path.push(host_binary_name("bin_a"));
let args = ["--bin".to_owned(), "bin_a".to_owned()];
let binary_artifact =
build_artifact(&work_dir, &args).expect("Failed to get artifact path.");
assert_eq!(binary_artifact.path(), expected_path);
}
#[test]
fn library_with_example() {
let work_dir = test_project_dir("library_with_example_project");
let args = [];
let binary_artifact = build_artifact(&work_dir, &args);
assert!(binary_artifact.is_err())
}
#[test]
fn library_with_example_specified() {
let work_dir = test_project_dir("library_with_example_project");
let mut expected_path = work_dir.join("target");
expected_path.push("debug");
expected_path.push("examples");
expected_path.push(host_binary_name("example"));
let args = owned_args(&["--example", "example"]);
let binary_artifact =
build_artifact(&work_dir, &args).expect("Failed to get artifact path.");
assert_eq!(binary_artifact.path(), expected_path);
}
fn test_project_dir(test_name: &str) -> PathBuf {
let mut manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
manifest_dir.push("src");
manifest_dir.push("bin");
manifest_dir.push("probe-rs");
manifest_dir.push("util");
manifest_dir.push("test_data");
manifest_dir.push(test_name);
dunce::canonicalize(manifest_dir).expect("Failed to build canonicalized test_project_dir")
}
fn owned_args(args: &[&str]) -> Vec<String> {
args.iter().map(|s| (*s).to_owned()).collect()
}
#[cfg(not(windows))]
fn host_binary_name(name: &str) -> String {
name.to_string()
}
#[cfg(windows)]
fn host_binary_name(name: &str) -> String {
name.to_string() + ".exe"
}
}