use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
fn main() {
println!("cargo:rerun-if-changed=build.rs");
let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("set by cargo"));
let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("set by cargo"));
let text = match linked(&manifest_dir) {
Ok(crates) if crates.is_empty() => "unknown: no ikigai-* crate in the lock\n".to_string(),
Ok(crates) => crates
.iter()
.map(|(name, version)| format!("{name} {version}\n"))
.collect(),
Err(why) => format!("unknown: {why}\n"),
};
std::fs::write(out_dir.join("linked-ikigai.txt"), text).expect("OUT_DIR is writable");
}
struct Locked {
name: String,
version: String,
path: bool,
dependencies: Vec<String>,
}
fn linked(manifest_dir: &Path) -> Result<BTreeSet<(String, String)>, String> {
let lock_path = manifest_dir
.ancestors()
.map(|dir| dir.join("Cargo.lock"))
.find(|path| path.is_file())
.ok_or("no Cargo.lock beside this package or above it")?;
println!("cargo:rerun-if-changed={}", lock_path.display());
let root = lock_path.parent().expect("a file has a parent");
let lock = read_toml(&lock_path)?;
let packages: Vec<Locked> = lock
.get("package")
.and_then(toml::Value::as_array)
.ok_or("the lock has no [[package]] table")?
.iter()
.filter_map(|p| {
Some(Locked {
name: p.get("name")?.as_str()?.to_string(),
version: p.get("version")?.as_str()?.to_string(),
path: p.get("source").is_none(),
dependencies: p
.get("dependencies")
.and_then(toml::Value::as_array)
.map(|deps| {
deps.iter()
.filter_map(|d| d.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default(),
})
})
.collect();
let members = member_manifests(root)?;
let workspace_deps = read_toml(&root.join("Cargo.toml"))
.ok()
.and_then(|t| t.get("workspace")?.get("dependencies")?.as_table().cloned())
.unwrap_or_default();
let this = std::env::var("CARGO_PKG_NAME").expect("set by cargo");
let this_version = std::env::var("CARGO_PKG_VERSION").expect("set by cargo");
let start = packages
.iter()
.position(|p| p.path && p.name == this && p.version == this_version)
.ok_or_else(|| format!("{this} {this_version} is not in {}", lock_path.display()))?;
let start_features = enabled_features(members.get(&this));
let registry_manifests = RegistryManifests::new();
let mut member_features: BTreeMap<usize, BTreeSet<String>> = BTreeMap::new();
let mut seen_registry: BTreeSet<usize> = BTreeSet::new();
let mut work: Vec<(usize, BTreeSet<String>)> = vec![(start, start_features)];
while let Some((at, requested)) = work.pop() {
let package = &packages[at];
let manifest = if package.path {
match members.get(&package.name) {
Some(manifest) => Some(manifest.clone()),
None => return Err(format!("no manifest found for member {}", package.name)),
}
} else if package.name.starts_with("ikigai-") {
registry_manifests.get(&package.name, &package.version)
} else {
None
};
let Some(manifest) = manifest else {
if seen_registry.insert(at) {
for dep in &package.dependencies {
for to in lock_targets(&packages, dep) {
work.push((to, BTreeSet::from(["default".to_string()])));
}
}
}
continue;
};
let manifest = &manifest;
let first = !member_features.contains_key(&at);
let known = member_features.entry(at).or_default();
let before = known.len();
known.extend(closure(manifest, &requested));
if !first && known.len() == before {
continue;
}
let features = known.clone();
for dep in normal_dependencies(manifest, &workspace_deps) {
if dep.optional && !turns_on(&features, &dep.key) {
continue;
}
let mut asked: BTreeSet<String> = dep.features.iter().cloned().collect();
if dep.default_features {
asked.insert("default".to_string());
}
for feature in &features {
if let Some((key, wanted)) = feature.split_once('/') {
if key.trim_end_matches('?') == dep.key {
asked.insert(wanted.to_string());
}
}
}
let targets: Vec<usize> = package
.dependencies
.iter()
.flat_map(|d| lock_targets(&packages, d))
.filter(|&to| packages[to].name == dep.package)
.collect();
for to in targets {
work.push((to, asked.clone()));
}
}
}
Ok(member_features
.keys()
.chain(seen_registry.iter())
.filter(|&&at| at != start)
.map(|&at| &packages[at])
.filter(|p| p.name.starts_with("ikigai-"))
.map(|p| (p.name.clone(), p.version.clone()))
.collect())
}
struct RegistryManifests {
roots: Vec<PathBuf>,
}
impl RegistryManifests {
fn new() -> Self {
let home = std::env::var_os("CARGO_HOME")
.map(PathBuf::from)
.or_else(|| {
std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(|home| PathBuf::from(home).join(".cargo"))
});
let roots = home
.and_then(|home| std::fs::read_dir(home.join("registry").join("src")).ok())
.map(|dirs| dirs.filter_map(|d| d.ok()).map(|d| d.path()).collect())
.unwrap_or_default();
RegistryManifests { roots }
}
fn get(&self, name: &str, version: &str) -> Option<toml::Table> {
self.roots
.iter()
.map(|root| root.join(format!("{name}-{version}")).join("Cargo.toml"))
.find(|path| path.is_file())
.and_then(|path| read_toml(&path).ok())
}
}
fn read_toml(path: &Path) -> Result<toml::Table, String> {
let text = std::fs::read_to_string(path)
.map_err(|e| format!("cannot read {}: {e}", path.display()))?;
text.parse::<toml::Table>()
.map_err(|e| format!("cannot parse {}: {e}", path.display()))
}
fn member_manifests(root: &Path) -> Result<BTreeMap<String, toml::Table>, String> {
let root_manifest = read_toml(&root.join("Cargo.toml"))?;
let mut dirs = vec![root.to_path_buf()];
let globs = root_manifest
.get("workspace")
.and_then(|w| w.get("members"))
.and_then(toml::Value::as_array)
.cloned()
.unwrap_or_default();
for glob in globs.iter().filter_map(toml::Value::as_str) {
match glob.strip_suffix("/*") {
Some(parent) => {
let parent = root.join(parent);
let entries = std::fs::read_dir(&parent)
.map_err(|e| format!("cannot list {}: {e}", parent.display()))?;
dirs.extend(entries.filter_map(|e| e.ok()).map(|e| e.path()));
}
None => dirs.push(root.join(glob)),
}
}
let mut members = BTreeMap::new();
for dir in dirs {
let path = dir.join("Cargo.toml");
if !path.is_file() {
continue;
}
let manifest = read_toml(&path)?;
let Some(name) = manifest
.get("package")
.and_then(|p| p.get("name"))
.and_then(toml::Value::as_str)
else {
continue;
};
println!("cargo:rerun-if-changed={}", path.display());
members.insert(name.to_string(), manifest);
}
Ok(members)
}
fn enabled_features(manifest: Option<&toml::Table>) -> BTreeSet<String> {
let declared = manifest
.and_then(|m| m.get("features"))
.and_then(toml::Value::as_table)
.map(|t| t.keys().cloned().collect::<Vec<_>>())
.unwrap_or_default();
let mut on: BTreeSet<String> = declared
.into_iter()
.filter(|name| {
let var = format!("CARGO_FEATURE_{}", name.to_uppercase().replace('-', "_"));
std::env::var_os(var).is_some()
})
.collect();
if let Some(manifest) = manifest {
for dep in normal_dependencies(manifest, &toml::Table::new()) {
let var = format!("CARGO_FEATURE_{}", dep.key.to_uppercase().replace('-', "_"));
if dep.optional && std::env::var_os(var).is_some() {
on.insert(dep.key);
}
}
}
on
}
fn closure(manifest: &toml::Table, requested: &BTreeSet<String>) -> BTreeSet<String> {
let table = manifest
.get("features")
.and_then(toml::Value::as_table)
.cloned()
.unwrap_or_default();
let mut on = BTreeSet::new();
let mut work: Vec<String> = requested.iter().cloned().collect();
while let Some(feature) = work.pop() {
if !on.insert(feature.clone()) {
continue;
}
if let Some(listed) = table.get(&feature).and_then(toml::Value::as_array) {
work.extend(listed.iter().filter_map(|v| v.as_str().map(str::to_string)));
}
}
on
}
fn turns_on(features: &BTreeSet<String>, key: &str) -> bool {
features.iter().any(|f| {
f == key
|| f.strip_prefix("dep:") == Some(key)
|| f.split_once('/').is_some_and(|(k, _)| k == key)
})
}
struct Dependency {
key: String,
package: String,
optional: bool,
default_features: bool,
features: Vec<String>,
}
fn normal_dependencies(manifest: &toml::Table, workspace: &toml::Table) -> Vec<Dependency> {
let mut tables: Vec<&toml::Table> = Vec::new();
if let Some(t) = manifest.get("dependencies").and_then(toml::Value::as_table) {
tables.push(t);
}
if let Some(targets) = manifest.get("target").and_then(toml::Value::as_table) {
for target in targets.values() {
if let Some(t) = target.get("dependencies").and_then(toml::Value::as_table) {
tables.push(t);
}
}
}
let mut deps = Vec::new();
for table in tables {
for (key, value) in table {
let own = value.as_table();
let inherited = own
.filter(|t| t.get("workspace").and_then(toml::Value::as_bool) == Some(true))
.and_then(|_| workspace.get(key))
.and_then(toml::Value::as_table);
let field = |name: &str| {
own.and_then(|t| t.get(name))
.or_else(|| inherited.and_then(|t| t.get(name)))
};
let flag = |names: [&str; 2], default: bool| {
names
.iter()
.find_map(|n| field(n).and_then(toml::Value::as_bool))
.unwrap_or(default)
};
let mut features: Vec<String> = Vec::new();
for t in [own, inherited].into_iter().flatten() {
if let Some(list) = t.get("features").and_then(toml::Value::as_array) {
features.extend(list.iter().filter_map(|v| v.as_str().map(str::to_string)));
}
}
deps.push(Dependency {
key: key.clone(),
package: field("package")
.and_then(toml::Value::as_str)
.unwrap_or(key)
.to_string(),
optional: own
.and_then(|t| t.get("optional"))
.and_then(toml::Value::as_bool)
.unwrap_or(false),
default_features: flag(["default-features", "default_features"], true),
features,
});
}
}
deps
}
fn lock_targets(packages: &[Locked], entry: &str) -> Vec<usize> {
let mut words = entry.split(' ');
let name = words.next().unwrap_or("");
let version = words.next();
packages
.iter()
.enumerate()
.filter(|(_, p)| p.name == name && version.is_none_or(|v| p.version == v))
.map(|(at, _)| at)
.collect()
}