use super::manifest::CargoToml;
use super::types::PlannedCompilationUnit;
use anyhow::{Context, Result};
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DepRequirement {
MetadataOnly,
FullArtifact,
}
pub fn package_has_build_script(manifest_path: &Path) -> bool {
let pkg_dir = manifest_path.parent().unwrap_or(Path::new("."));
if let Ok(pkg_toml) = CargoToml::from_file(manifest_path) {
match pkg_toml.package.as_ref().and_then(|p| p.build.as_ref()) {
Some(super::manifest::TomlBuild::Disabled(false)) => false,
Some(super::manifest::TomlBuild::Path(ref rel_p)) => pkg_dir.join(rel_p).is_file(),
_ => {
pkg_dir.join("build.rs").is_file()
|| pkg_dir.join("build").join("build.rs").is_file()
}
}
} else {
pkg_dir.join("build.rs").is_file() || pkg_dir.join("build").join("build.rs").is_file()
}
}
pub fn classify_dep_requirement(
consumer: &PlannedCompilationUnit,
dependency: &PlannedCompilationUnit,
) -> DepRequirement {
if dependency.kind == "proc-macro" {
return DepRequirement::FullArtifact;
}
let is_consumer_linking = matches!(
consumer.kind.as_str(),
"bin" | "test" | "custom-test" | "proc-macro" | "dylib" | "cdylib" | "example" | "bench"
);
if is_consumer_linking {
return DepRequirement::FullArtifact;
}
if package_has_build_script(&consumer.manifest_path) {
if dependency.for_host {
return DepRequirement::FullArtifact;
}
if let Ok(toml) = CargoToml::from_file(&consumer.manifest_path) {
let dep_pkg = dependency.package_name.replace('-', "_");
let matches_dep = |(k, d): (&String, &super::manifest::TomlDependency)| {
k.replace('-', "_") == dep_pkg
|| matches!(
d,
super::manifest::TomlDependency::Detailed(dt)
if dt
.package_name
.as_deref()
.is_some_and(|p| p.replace('-', "_") == dep_pkg)
)
};
let is_build_dep = toml.build_dependencies.iter().any(matches_dep)
|| toml
.target
.values()
.any(|t| t.build_dependencies.iter().any(matches_dep));
if is_build_dep {
return DepRequirement::FullArtifact;
}
}
}
DepRequirement::MetadataOnly
}
pub fn parse_rustc_artifact_line(line: &str) -> Option<(PathBuf, String)> {
if !line.contains("\"$message_type\":\"artifact\"") {
return None;
}
if let Ok(v) = serde_json::from_str::<serde_json::Value>(line) {
if v.get("$message_type").and_then(|s| s.as_str()) == Some("artifact") {
let artifact = v.get("artifact").and_then(|s| s.as_str())?;
let emit = v
.get("emit")
.and_then(|s| s.as_str())
.unwrap_or("")
.to_string();
return Some((PathBuf::from(artifact), emit));
}
}
None
}
pub fn parse_rustc_rendered_diagnostic(line: &str) -> Option<String> {
if !line.contains("\"$message_type\":\"diagnostic\"") {
return None;
}
if let Ok(v) = serde_json::from_str::<serde_json::Value>(line) {
if let Some(rendered) = v.get("rendered").and_then(|s| s.as_str()) {
return Some(rendered.to_string());
}
}
None
}
pub struct PipelinedRustcResult {
pub success: bool,
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
pub primary_output: Option<PathBuf>,
pub metadata_output: Option<PathBuf>,
}
pub fn run_pipelined_rustc<F>(mut cmd: Command, on_metadata: F) -> Result<PipelinedRustcResult>
where
F: FnOnce(&Path),
{
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let mut child = cmd.spawn().context("Failed to spawn rustc child process")?;
let stdout_pipe = child.stdout.take().context("Failed to capture stdout")?;
let stderr_pipe = child.stderr.take().context("Failed to capture stderr")?;
let stdout_handle = std::thread::spawn(move || {
let mut out = String::new();
let reader = BufReader::new(stdout_pipe);
for line in reader.lines().map_while(Result::ok) {
out.push_str(&line);
out.push('\n');
}
out
});
let mut on_meta_opt = Some(on_metadata);
let mut collected_stderr = String::new();
let mut primary_output = None;
let mut metadata_output = None;
let stderr_reader = BufReader::new(stderr_pipe);
for line in stderr_reader.lines().map_while(Result::ok) {
if let Some((artifact_path, emit)) = parse_rustc_artifact_line(&line) {
if emit == "metadata" {
metadata_output = Some(artifact_path.clone());
if let Some(cb) = on_meta_opt.take() {
cb(&artifact_path);
}
} else if emit == "link" {
primary_output = Some(artifact_path);
}
} else if let Some(rendered) = parse_rustc_rendered_diagnostic(&line) {
collected_stderr.push_str(&rendered);
} else {
collected_stderr.push_str(&line);
collected_stderr.push('\n');
}
}
let status = child
.wait()
.context("Failed to wait on rustc child process")?;
let stdout_str = stdout_handle
.join()
.unwrap_or_else(|_| "<stdout thread panic>".to_string());
let exit_code = status
.code()
.unwrap_or(if status.success() { 0 } else { 1 });
let success = status.success();
Ok(PipelinedRustcResult {
success,
exit_code,
stdout: stdout_str,
stderr: collected_stderr,
primary_output,
metadata_output,
})
}