use crate::Result;
use crate::generator::write_if_changed;
use std::path::{Path, PathBuf};
pub const PLUSHIE_RUST_CRATES: &[&str] = &[
"plushie-core",
"plushie-core-macros",
"plushie-renderer",
"plushie-renderer-lib",
"plushie-widget-sdk",
];
pub fn forwarded_patches(source_path: &Path) -> Vec<(String, PathBuf)> {
let mut out: Vec<(String, PathBuf)> = Vec::new();
let sources = [
source_path.join("Cargo.toml"),
source_path.join(".cargo/config.toml"),
];
for manifest in &sources {
let Ok(contents) = std::fs::read_to_string(manifest) else {
continue;
};
let Ok(parsed) = contents.parse::<toml_edit::DocumentMut>() else {
continue;
};
let Some(patch) = parsed.get("patch").and_then(|p| p.get("crates-io")) else {
continue;
};
let Some(table) = patch.as_table() else {
continue;
};
for (name, item) in table.iter() {
if out.iter().any(|(existing, _)| existing == name) {
continue;
}
let entry = item.as_inline_table().map(|t| t.clone().into_table());
let Some(entry) = entry else {
continue;
};
let Some(path_value) = entry.get("path").and_then(|v| v.as_str()) else {
continue;
};
let resolved = source_path.join(path_value);
if resolved.is_dir() {
out.push((name.to_string(), resolved));
}
}
}
out
}
pub fn all_patches(source_path: &Path) -> Vec<(String, PathBuf)> {
let mut out: Vec<(String, PathBuf)> = PLUSHIE_RUST_CRATES
.iter()
.map(|name| {
let path = source_path.join("crates").join(name);
((*name).to_string(), path)
})
.collect();
for (name, path) in forwarded_patches(source_path) {
if PLUSHIE_RUST_CRATES.contains(&name.as_str()) {
continue;
}
out.push((name, path));
}
out
}
pub(crate) fn render_patch_block(entries: &[(String, PathBuf)]) -> String {
let mut out = String::from("[patch.crates-io]\n");
for (name, path) in entries {
out.push_str(&format!(
"{name} = {{ path = {:?} }}\n",
path.display().to_string()
));
}
out
}
pub fn write_scratch_cargo_config(spec_manifest_dir: &Path, source_path: &Path) -> Result<()> {
let entries = all_patches(source_path);
let body = format!(
"# Auto-generated by `cargo plushie build`. Do not edit.\n\
# Redirects plushie-rust crates.io deps to a local checkout so\n\
# `cargo metadata` can resolve unpublished workspace versions.\n\n\
{}",
render_patch_block(&entries)
);
let path = spec_manifest_dir.join(".cargo/config.toml");
write_if_changed(&path, &body)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
fn populate_source_checkout(source_root: &Path) {
for name in PLUSHIE_RUST_CRATES {
std::fs::create_dir_all(source_root.join("crates").join(name)).unwrap();
}
}
#[test]
fn all_patches_emits_every_plushie_rust_crate() {
let dir = tempdir().unwrap();
populate_source_checkout(dir.path());
let entries = all_patches(dir.path());
let names: Vec<&str> = entries.iter().map(|(n, _)| n.as_str()).collect();
for crate_name in PLUSHIE_RUST_CRATES {
assert!(
names.contains(crate_name),
"missing patch for `{crate_name}`: got {names:?}"
);
}
for (name, path) in &entries {
assert_eq!(
path,
&dir.path().join("crates").join(name),
"path for `{name}` should resolve under source checkout"
);
}
}
#[test]
fn all_patches_forwards_non_plushie_entries() {
let dir = tempdir().unwrap();
populate_source_checkout(dir.path());
std::fs::create_dir_all(dir.path().join("../plushie-iced-sibling")).unwrap();
let config_toml = r#"
[patch.crates-io]
plushie-iced = { path = "../plushie-iced-sibling" }
"#;
std::fs::create_dir_all(dir.path().join(".cargo")).unwrap();
std::fs::write(dir.path().join(".cargo/config.toml"), config_toml).unwrap();
let entries = all_patches(dir.path());
let iced = entries
.iter()
.find(|(n, _)| n == "plushie-iced")
.expect("plushie-iced forwarded");
assert!(
iced.1.ends_with("plushie-iced-sibling"),
"forwarded path resolves relative to source root: {:?}",
iced.1
);
}
#[test]
fn all_patches_drops_plushie_rust_entries_from_forwarded_sources() {
let dir = tempdir().unwrap();
populate_source_checkout(dir.path());
let cargo_toml = r#"
[workspace]
members = []
[patch.crates-io]
plushie-widget-sdk = { path = "some/weird/other/path" }
"#;
std::fs::create_dir_all(dir.path().join("some/weird/other/path")).unwrap();
std::fs::write(dir.path().join("Cargo.toml"), cargo_toml).unwrap();
let entries = all_patches(dir.path());
let sdk_entries: Vec<&PathBuf> = entries
.iter()
.filter(|(n, _)| n == "plushie-widget-sdk")
.map(|(_, p)| p)
.collect();
assert_eq!(
sdk_entries.len(),
1,
"only the canonical plushie-widget-sdk patch survives"
);
assert_eq!(
sdk_entries[0],
&dir.path().join("crates/plushie-widget-sdk")
);
}
#[test]
fn write_scratch_cargo_config_emits_all_plushie_patches() {
let source = tempdir().unwrap();
populate_source_checkout(source.path());
let spec = tempdir().unwrap();
write_scratch_cargo_config(spec.path(), source.path()).unwrap();
let config_path = spec.path().join(".cargo/config.toml");
let body = std::fs::read_to_string(&config_path).unwrap();
assert!(body.contains("[patch.crates-io]"));
for name in PLUSHIE_RUST_CRATES {
let expected_path = source.path().join("crates").join(name);
let expected_line = format!(
"{name} = {{ path = {:?} }}",
expected_path.display().to_string()
);
assert!(
body.contains(&expected_line),
"config should contain `{expected_line}`\nactual:\n{body}"
);
}
}
#[test]
fn write_scratch_cargo_config_forwards_plushie_iced_patch() {
let source = tempdir().unwrap();
populate_source_checkout(source.path());
std::fs::create_dir_all(source.path().join("../plushie-iced-sibling")).unwrap();
std::fs::create_dir_all(source.path().join(".cargo")).unwrap();
let src_config = r#"
[patch.crates-io]
plushie-iced = { path = "../plushie-iced-sibling" }
"#;
std::fs::write(source.path().join(".cargo/config.toml"), src_config).unwrap();
let spec = tempdir().unwrap();
write_scratch_cargo_config(spec.path(), source.path()).unwrap();
let body = std::fs::read_to_string(spec.path().join(".cargo/config.toml")).unwrap();
assert!(
body.contains("plushie-iced = {"),
"forwarded plushie-iced patch should appear in scratch config:\n{body}"
);
}
#[test]
fn write_scratch_cargo_config_is_idempotent() {
let source = tempdir().unwrap();
populate_source_checkout(source.path());
let spec = tempdir().unwrap();
write_scratch_cargo_config(spec.path(), source.path()).unwrap();
let config_path = spec.path().join(".cargo/config.toml");
let mtime1 = std::fs::metadata(&config_path).unwrap().modified().unwrap();
std::thread::sleep(std::time::Duration::from_millis(20));
write_scratch_cargo_config(spec.path(), source.path()).unwrap();
let mtime2 = std::fs::metadata(&config_path).unwrap().modified().unwrap();
assert_eq!(
mtime1, mtime2,
"write_if_changed must skip identical content to preserve mtime"
);
}
}