use std::collections::BTreeMap;
use std::path::Path;
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct Manifest {
package: Package,
#[serde(default)]
dependencies: BTreeMap<String, Dep>,
#[serde(default)]
target: BTreeMap<String, TargetDeps>,
}
#[derive(Deserialize)]
struct Package {
metadata: Metadata,
}
#[derive(Deserialize)]
struct Metadata {
rustscript: Rustscript,
}
#[derive(Deserialize)]
struct Rustscript {
#[serde(rename = "script-crates")]
script_crates: Vec<String>,
}
#[derive(Deserialize, Serialize, Default)]
struct TargetDeps {
#[serde(default)]
dependencies: BTreeMap<String, Dep>,
}
#[derive(Deserialize)]
struct WorkspaceManifest {
workspace: Workspace,
}
#[derive(Deserialize)]
struct Workspace {
dependencies: BTreeMap<String, Dep>,
}
#[derive(Deserialize, Serialize, Clone)]
#[serde(untagged)]
enum Dep {
Version(String),
Table(DepTable),
}
#[derive(Deserialize, Serialize, Clone, Default)]
struct DepTable {
#[serde(default, skip_serializing_if = "Option::is_none")]
version: Option<String>,
#[serde(default, skip_serializing)]
workspace: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
features: Vec<String>,
#[serde(
default,
rename = "default-features",
skip_serializing_if = "Option::is_none"
)]
default_features: Option<bool>,
}
#[derive(Serialize)]
struct ScriptManifest {
dependencies: BTreeMap<String, Dep>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
target: BTreeMap<String, TargetDeps>,
}
pub struct Generated {
pub manifest: String,
pub crates_rs: String,
}
pub fn generate(manifest_dir: &Path) -> Generated {
let manifest = read_manifest(manifest_dir);
let workspace = read_workspace(manifest_dir);
let wanted = &manifest.package.metadata.rustscript.script_crates;
let mut dependencies = BTreeMap::new();
let mut target: BTreeMap<String, TargetDeps> = BTreeMap::new();
for name in wanted {
if let Some(dep) = manifest.dependencies.get(name) {
dependencies.insert(name.clone(), resolve(name, dep, workspace.as_ref()));
continue;
}
let found = manifest
.target
.iter()
.find_map(|(cfg, deps)| deps.dependencies.get(name).map(|dep| (cfg, dep)));
let Some((cfg, dep)) = found else {
panic!("script crate `{name}` is not a dependency of run-rs");
};
target
.entry(cfg.clone())
.or_default()
.dependencies
.insert(name.clone(), resolve(name, dep, workspace.as_ref()));
}
let manifest = toml::to_string(&ScriptManifest {
dependencies,
target,
})
.expect("the script manifest serializes");
let names: Vec<String> = wanted
.iter()
.map(|name| format!("{:?}", name.replace('-', "_")))
.collect();
let crates_rs = format!(
"// Generated by build.rs from Cargo.toml. Do not edit.\n\n\
/// The crates a script may use without declaring anything.\n\
pub const SCRIPT_CRATES: &[&str] = &[{}];\n",
names.join(", ")
);
Generated {
manifest,
crates_rs,
}
}
fn read_manifest(manifest_dir: &Path) -> Manifest {
let path = manifest_dir.join("Cargo.toml");
let text = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
toml::from_str(&text).unwrap_or_else(|e| panic!("cannot parse {}: {e}", path.display()))
}
fn read_workspace(manifest_dir: &Path) -> Option<BTreeMap<String, Dep>> {
let path = manifest_dir.join("../../Cargo.toml");
let text = std::fs::read_to_string(&path).ok()?;
let parsed: WorkspaceManifest =
toml::from_str(&text).unwrap_or_else(|e| panic!("cannot parse {}: {e}", path.display()));
Some(parsed.workspace.dependencies)
}
fn resolve(name: &str, dep: &Dep, workspace: Option<&BTreeMap<String, Dep>>) -> Dep {
let Dep::Table(table) = dep else {
return dep.clone();
};
if !table.workspace {
return dep.clone();
}
let base = workspace
.and_then(|deps| deps.get(name))
.unwrap_or_else(|| {
panic!("`{name}` inherits from the workspace but the workspace manifest has no entry")
});
let mut merged = match base {
Dep::Version(version) => DepTable {
version: Some(version.clone()),
..DepTable::default()
},
Dep::Table(base) => base.clone(),
};
merged.workspace = false;
merged.features.extend(table.features.iter().cloned());
if table.default_features.is_some() {
merged.default_features = table.default_features;
}
if merged.features.is_empty() && merged.default_features.is_none() {
return Dep::Version(merged.version.unwrap_or_default());
}
Dep::Table(merged)
}