use super::build_plan::BuildPlan;
use crate::language::Language;
use crate::project::CrateDependency;
pub struct CargoManifest<'p> {
kind: CargoTargetKind,
target_name: String,
dependencies: &'p [CrateDependency],
dev_dependencies: &'p [CrateDependency],
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum CargoTargetKind {
Bin,
Test,
}
struct TargetShape {
flag: &'static str,
table: &'static str,
harness: &'static str,
name_part: &'static str,
}
impl CargoTargetKind {
const BIN: TargetShape = TargetShape {
flag: "--bin",
table: "[[bin]]",
harness: "",
name_part: "",
};
const TEST: TargetShape = TargetShape {
flag: "--test",
table: "[[test]]",
harness: "harness = false\n",
name_part: "-tests",
};
pub fn target_name(self, name_stem: &str, root_hash: &str) -> String {
format!("{name_stem}{}-{root_hash}", self.shape().name_part)
}
fn shape(self) -> &'static TargetShape {
match self {
Self::Bin => &Self::BIN,
Self::Test => &Self::TEST,
}
}
}
impl<'p> CargoManifest<'p> {
pub fn new(
kind: CargoTargetKind,
target_name: String,
dependencies: &'p [CrateDependency],
dev_dependencies: &'p [CrateDependency],
) -> Self {
Self {
kind,
target_name,
dependencies,
dev_dependencies,
}
}
pub fn target_arguments(&self) -> [&str; 2] {
[self.kind.shape().flag, &self.target_name]
}
pub fn names_target(&self, name: &str) -> bool {
self.target_name == name
}
}
impl CargoManifest<'_> {
const PACKAGE_SUFFIX: &'static str = "-program";
const SOURCE_PATH: &'static str = "src/main.rs";
pub fn package_name() -> String {
format!("{}{}", Language::NAME, Self::PACKAGE_SUFFIX)
}
pub fn render(&self, plan: &BuildPlan) -> String {
let mut text = format!(
"[package]\nname = {}\nversion = \"0.1.0\"\nedition = {}\npublish = false\n\
autobins = false\nautotests = false\nautoexamples = false\nautobenches = false\n\n\
{}\nname = {}\npath = {}\n{}\n[workspace]\n\n{}\n",
Self::quoted(&Self::package_name()),
Self::quoted(BuildPlan::EDITION),
self.kind.shape().table,
Self::quoted(&self.target_name),
Self::quoted(Self::SOURCE_PATH),
self.kind.shape().harness,
plan.manifest_profile(),
);
Self::write_section(&mut text, "dependencies", self.dependencies);
Self::write_section(&mut text, "dev-dependencies", self.dev_dependencies);
text
}
fn write_section(text: &mut String, section: &str, crates: &[CrateDependency]) {
if crates.is_empty() {
return;
}
text.push_str(&format!("[{section}]\n"));
crates
.iter()
.for_each(|dependency| text.push_str(&Self::entry(dependency)));
text.push('\n');
}
fn entry(dependency: &CrateDependency) -> String {
let mut fields = Vec::new();
if let Some(version) = &dependency.version {
fields.push(format!("version = {}", Self::quoted(version)));
}
if let Some(path) = &dependency.path {
fields.push(format!(
"path = {}",
Self::quoted(&path.display().to_string())
));
}
if !dependency.features.is_empty() {
let features: Vec<String> = dependency
.features
.iter()
.map(|feature| Self::quoted(feature))
.collect();
fields.push(format!("features = [{}]", features.join(", ")));
}
format!(
"{} = {{ {} }}\n",
Self::quoted(&dependency.name),
fields.join(", ")
)
}
fn quoted(text: &str) -> String {
let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{escaped}\"")
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::{CargoManifest, CargoTargetKind};
use crate::build::{BuildPlan, BuildProfile};
use crate::project::CrateDependency;
use crate::source::{SourceMap, Span};
fn dependency(
name: &str,
version: Option<&str>,
features: &[&str],
path: Option<&str>,
) -> CrateDependency {
let mut sources = SourceMap::default();
let source = sources.add("project", "(fake)");
CrateDependency {
name: name.to_string(),
name_span: Span::new(source, 1..5),
version: version.map(str::to_string),
features: features.iter().map(|feature| feature.to_string()).collect(),
path: path.map(PathBuf::from),
}
}
#[test]
fn the_manifest_writes_each_field_of_an_entry() {
let dependencies = [
dependency("fake-json", Some("1.2"), &["derive", "std"], None),
dependency("fake_local", None, &[], Some("/fixtures/fake \"local\"")),
];
let dev_dependencies = [dependency("fake-check", Some("0.3"), &[], None)];
let manifest = CargoManifest::new(
CargoTargetKind::Bin,
"demo-00000000".to_string(),
&dependencies,
&dev_dependencies,
);
let text = manifest.render(&BuildPlan::new(BuildProfile::Debug));
assert!(text.contains("[dependencies]\n\"fake-json\" = { version = \"1.2\", features = [\"derive\", \"std\"] }\n"));
assert!(text.contains("\"fake_local\" = { path = \"/fixtures/fake \\\"local\\\"\" }\n"));
assert!(text.contains("[dev-dependencies]\n\"fake-check\" = { version = \"0.3\" }\n"));
assert!(text.contains("[[bin]]\nname = \"demo-00000000\"\n"));
}
#[test]
fn the_manifest_leaves_out_an_empty_section() {
let manifest =
CargoManifest::new(CargoTargetKind::Bin, "demo-00000000".to_string(), &[], &[]);
let text = manifest.render(&BuildPlan::new(BuildProfile::Release));
assert!(!text.contains("dependencies]"));
assert!(text.contains("[profile.dev]\ndebug = false\n"));
}
#[test]
fn a_target_name_joins_the_project_and_the_root_hash() {
assert_eq!(
CargoTargetKind::Bin.target_name("demo", "0000abcd"),
"demo-0000abcd"
);
assert_eq!(
CargoTargetKind::Test.target_name("demo", "0000abcd"),
"demo-tests-0000abcd"
);
}
#[test]
fn a_test_target_runs_its_own_main() {
let manifest = CargoManifest::new(
CargoTargetKind::Test,
"demo-tests-00000000".to_string(),
&[],
&[],
);
let text = manifest.render(&BuildPlan::new(BuildProfile::Debug));
assert!(text.contains(
"[[test]]\nname = \"demo-tests-00000000\"\npath = \"src/main.rs\"\nharness = false\n\n[workspace]\n"
));
assert!(!text.contains("[[bin]]"));
}
}