use crate::{
Result,
builder::{BuildOptions, BuildTarget},
error,
};
use snafu::{OptionExt, ResultExt};
use std::{
path::{Path, PathBuf},
process::Command,
};
pub(crate) use cargo_metadata::Metadata;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum CargoVerbosity {
#[default]
Normal,
Verbose,
VeryVerbose,
ExtremelyVerbose,
}
impl CargoVerbosity {
pub(crate) fn from_count(count: u8) -> Self {
match count {
0 => Self::Normal,
1 => Self::Verbose,
2 => Self::VeryVerbose,
_ => Self::ExtremelyVerbose,
}
}
}
#[derive(Clone, Debug, Default)]
pub(crate) struct CargoMetadataOptions {
pub no_deps: bool,
pub filter_platform: Option<String>,
pub features: Vec<String>,
pub all_features: bool,
pub no_default_features: bool,
pub offline: bool,
pub locked: bool,
}
impl From<&BuildOptions> for CargoMetadataOptions {
fn from(opts: &BuildOptions) -> Self {
Self {
no_deps: false,
filter_platform: opts.target.clone(),
features: opts.features.clone(),
all_features: opts.all_features,
no_default_features: opts.no_default_features,
offline: opts.offline,
locked: opts.locked,
}
}
}
pub(crate) trait CargoRunner: std::fmt::Debug + Send + Sync + 'static {
fn metadata(&self, source_dir: &Path, options: &CargoMetadataOptions) -> Result<Metadata>;
fn build(&self, source_dir: &Path, package: Option<&str>, options: &BuildOptions) -> Result<PathBuf>;
}
pub(crate) fn find_cargo() -> Result<impl CargoRunner> {
let cargo_path = find_executable("cargo", "CARGO")?;
let rustup_path = find_executable("rustup", "RUSTUP").ok();
Ok(RealCargoRunner {
cargo_path,
rustup_path,
})
}
#[derive(Debug, Clone)]
struct RealCargoRunner {
cargo_path: PathBuf,
rustup_path: Option<PathBuf>,
}
impl CargoRunner for RealCargoRunner {
fn metadata(&self, source_dir: &Path, options: &CargoMetadataOptions) -> Result<Metadata> {
use snafu::ResultExt;
let mut cmd = cargo_metadata::MetadataCommand::new();
cmd.cargo_path(&self.cargo_path).current_dir(source_dir);
if options.no_deps {
cmd.no_deps();
}
if options.all_features {
cmd.features(cargo_metadata::CargoOpt::AllFeatures);
} else {
if options.no_default_features {
cmd.features(cargo_metadata::CargoOpt::NoDefaultFeatures);
}
if !options.features.is_empty() {
cmd.features(cargo_metadata::CargoOpt::SomeFeatures(options.features.clone()));
}
}
let mut other_args = Vec::new();
let platform: Option<&str> = if options.no_deps {
options.filter_platform.as_deref()
} else {
Some(
options
.filter_platform
.as_deref()
.unwrap_or(build_context::TARGET),
)
};
if let Some(platform_str) = platform {
other_args.push("--filter-platform".to_string());
other_args.push(platform_str.to_string());
}
if options.offline {
other_args.push("--offline".to_string());
}
if options.locked {
other_args.push("--locked".to_string());
}
if !other_args.is_empty() {
cmd.other_options(other_args);
}
cmd.exec().with_context(|_| error::CargoMetadataSnafu {
cargo_path: self.cargo_path.clone(),
source_dir: source_dir.to_path_buf(),
})
}
fn build(&self, source_dir: &Path, package: Option<&str>, options: &BuildOptions) -> Result<PathBuf> {
if !source_dir.join("Cargo.toml").exists() {
return error::CargoTomlNotFoundSnafu {
source_dir: source_dir.to_path_buf(),
}
.fail();
}
let mut cmd = if let Some(toolchain) = &options.toolchain {
let rustup_path = self
.rustup_path
.as_ref()
.with_context(|| error::RustupNotFoundSnafu {
toolchain: toolchain.clone(),
})?;
let mut cmd = Command::new(rustup_path);
cmd.args(["run", toolchain, "cargo"]);
cmd
} else {
Command::new(&self.cargo_path)
};
cmd.arg("build");
cmd.current_dir(source_dir);
cmd.arg("--message-format=json");
if let Some(profile) = &options.profile {
cmd.args(["--profile", profile]);
} else {
cmd.arg("--release");
}
if let Some(pkg) = package {
cmd.args(["-p", pkg]);
}
if options.all_features {
cmd.arg("--all-features");
} else {
if options.no_default_features {
cmd.arg("--no-default-features");
}
if !options.features.is_empty() {
cmd.arg("--features");
cmd.arg(options.features.join(","));
}
}
if let Some(target) = &options.target {
cmd.args(["--target", target]);
}
match &options.build_target {
BuildTarget::DefaultBin => {
}
BuildTarget::Bin(name) => {
cmd.args(["--bin", name]);
}
BuildTarget::Example(name) => {
cmd.args(["--example", name]);
}
}
if options.locked {
cmd.arg("--locked");
}
if options.offline {
cmd.arg("--offline");
}
if let Some(jobs) = options.jobs {
cmd.args(["-j", &jobs.to_string()]);
}
if options.ignore_rust_version {
cmd.arg("--ignore-rust-version");
}
match options.cargo_verbosity {
CargoVerbosity::Normal => {}
CargoVerbosity::Verbose => {
cmd.arg("-v");
}
CargoVerbosity::VeryVerbose => {
cmd.arg("-vv");
}
CargoVerbosity::ExtremelyVerbose => {
cmd.arg("-vvv");
}
}
let output = cmd.output().context(error::CommandExecutionSnafu)?;
if !output.status.success() {
return error::CargoBuildFailedSnafu {
exit_code: output.status.code(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
}
.fail();
}
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if let Ok(msg) = serde_json::from_str::<serde_json::Value>(line) {
if msg.get("reason").and_then(|r| r.as_str()) == Some("compiler-artifact") {
let target = msg.get("target");
let kinds = target.and_then(|t| t.get("kind")).and_then(|k| k.as_array());
let name = target.and_then(|t| t.get("name")).and_then(|n| n.as_str());
let matches = match &options.build_target {
BuildTarget::DefaultBin => {
kinds.is_some_and(|k| k.iter().any(|v| v.as_str() == Some("bin")))
}
BuildTarget::Bin(bin_name) => {
kinds.is_some_and(|k| k.iter().any(|v| v.as_str() == Some("bin")))
&& name == Some(bin_name.as_str())
}
BuildTarget::Example(ex_name) => {
kinds.is_some_and(|k| k.iter().any(|v| v.as_str() == Some("example")))
&& name == Some(ex_name.as_str())
}
};
if matches {
if let Some(exe) = msg.get("executable").and_then(|e| e.as_str()) {
return Ok(PathBuf::from(exe));
}
}
}
}
}
error::BinaryNotFoundInOutputSnafu.fail()
}
}
fn find_executable(name: &str, env_var: &str) -> Result<PathBuf> {
if let Ok(path) = std::env::var(env_var) {
let path = PathBuf::from(path);
if path.exists() {
return Ok(path);
}
}
if let Ok(path) = which::which(name) {
return Ok(path);
}
let cargo_home = std::env::var("CARGO_HOME")
.ok()
.map(PathBuf::from)
.or_else(|| home::cargo_home().ok());
if let Some(cargo_home) = cargo_home {
let path = cargo_home.join("bin").join(name);
if path.exists() {
return Ok(path);
}
}
error::ExecutableNotFoundSnafu {
name: name.to_string(),
}
.fail()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{builder::BuildTarget, testdata::CrateTestCase};
fn cgx_project_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("cgx-core should have a parent directory (workspace root)")
.to_path_buf()
}
#[test]
fn find_cargo_succeeds() {
crate::logging::init_test_logging();
let _cargo = find_cargo().unwrap();
}
#[test]
fn metadata_reads_cgx_crate() {
crate::logging::init_test_logging();
let cargo = find_cargo().unwrap();
let cgx_root = cgx_project_root();
let metadata = cargo
.metadata(
&cgx_root,
&CargoMetadataOptions {
no_deps: true,
..Default::default()
},
)
.unwrap();
let cgx_pkg = metadata
.packages
.iter()
.find(|p| p.name.as_str() == "cgx")
.unwrap();
assert_eq!(cgx_pkg.name.as_str(), "cgx");
assert!(!cgx_pkg.version.to_string().is_empty());
let has_bin = cgx_pkg
.targets
.iter()
.any(|t| t.kind.iter().any(|k| k.to_string() == "bin"));
assert!(has_bin, "cgx should have a binary target");
}
#[test]
fn build_compiles_cgx_in_tempdir() {
crate::logging::init_test_logging();
let cargo = find_cargo().unwrap();
let cgx_root = cgx_project_root();
let temp_dir = tempfile::tempdir().unwrap();
crate::helpers::copy_source_tree(&cgx_root, temp_dir.path()).unwrap();
assert!(
temp_dir.path().join("Cargo.toml").exists(),
"Cargo.toml should be copied"
);
let options = BuildOptions {
profile: Some("dev".to_string()),
build_target: BuildTarget::DefaultBin,
..Default::default()
};
let binary_path = cargo.build(temp_dir.path(), Some("cgx"), &options).unwrap();
assert!(binary_path.exists(), "Binary should exist at {:?}", binary_path);
assert!(binary_path.is_file(), "Binary should be a file");
let file_name = binary_path.file_name().and_then(|n| n.to_str()).unwrap();
assert!(
file_name == "cgx" || file_name == "cgx.exe",
"Binary should be named cgx or cgx.exe, got {}",
file_name
);
}
#[test]
fn metadata_loads_all_testcases() {
crate::logging::init_test_logging();
let cargo = find_cargo().unwrap();
for testcase in CrateTestCase::all() {
let result = cargo.metadata(testcase.path(), &CargoMetadataOptions::default());
assert!(
result.is_ok(),
"Failed to load metadata for {}: {:?}",
testcase.name,
result.err()
);
}
}
}