Skip to main content

yog_cli/
cargo_toml.rs

1//! The generated `.yog-build/Cargo.toml`.
2
3use crate::manifest::{DEFAULT_EDITION, DEPS_SECTION, YogToml, to_snake_name};
4use std::fmt::Write as _;
5use std::path::{Path, PathBuf};
6
7/// Generate the hidden Cargo.toml from yog.toml metadata.
8/// `benches`: the mod's `yog bench` files — they add `[dev-dependencies]`,
9/// `[[bench]]` targets and an `rlib` for them to link.
10pub fn generate_cargo_toml(meta: &YogToml, benches: &[crate::bench::BenchFile]) -> String {
11    let authors_toml = authors_line(meta);
12    let edition = meta.edition.as_deref().unwrap_or(DEFAULT_EDITION);
13    let patch = local_patch(meta);
14    let deps_lines = dependency_lines(meta);
15    format!(
16        r#"# Generated by yog from yog.toml — do not edit.
17[package]
18name         = "{id}"
19version      = "{version}"
20edition      = "{edition}"
21description  = {description:?}
22{authors_line}license      = {license:?}
23
24[lib]
25name       = "{lib_name}"
26crate-type = {crate_types}
27path       = "../src/lib.rs"
28
29[dependencies]
30{api_dep}
31{deps}
32{bench}{patch}"#,
33        id = meta.id,
34        lib_name = to_snake_name(&meta.id),
35        version = meta.version,
36        edition = edition,
37        description = meta.description,
38        authors_line = authors_toml,
39        license = meta.license,
40        api_dep = meta.api_dep(),
41        deps = deps_lines.join("\n"),
42        patch = patch,
43        crate_types = if benches.is_empty() {
44            r#"["cdylib"]"#
45        } else {
46            r#"["cdylib", "rlib"]"#
47        },
48        bench = crate::bench::cargo_sections(meta, benches, &test_support(meta)),
49    )
50}
51
52/// `authors = [...]`, when the mod names any.
53fn authors_line(meta: &YogToml) -> String {
54    if meta.authors.is_empty() {
55        return String::new();
56    }
57    let list = meta
58        .authors
59        .iter()
60        .map(|a| format!("{a:?}"))
61        .collect::<Vec<_>>()
62        .join(", ");
63    format!("authors      = [{list}]\n")
64}
65
66/// With a local yog-api, its sibling yog-* crates (yog-ui, yog-gfx, ...)
67/// come from the same checkout too — the local API may need versions of
68/// them that aren't on crates.io yet; so do the mod's own yog-*
69/// dependencies (yog-kit, ...), and yog-api for them.
70fn local_patch(meta: &YogToml) -> String {
71    let env_path = std::env::var(yog_ids::env::API_PATH).ok();
72    let Some(api_path) = meta.yog_api_path.as_deref().or(env_path.as_deref()) else {
73        return String::new();
74    };
75    let mut extra: Vec<&str> = meta
76        .dependencies
77        .iter()
78        .map(|(name, _)| name.as_str())
79        .filter(|n| n.starts_with(YOG_CRATE))
80        .collect();
81    if !extra.is_empty() {
82        extra.push(YOG_API);
83    }
84    local_api_patch(Path::new(api_path), &extra)
85}
86
87use yog_ids::rust_crate::{API as YOG_API, PREFIX as YOG_CRATE};
88
89/// `[dependencies]`: plain crates.io ones exactly as written, then other
90/// Yog mods' (`[dependencies.exports]`) — each its exports crate under the
91/// mod's own name, so mod code imports it as a direct dependency
92/// (`use {name}::...`) and two never collide.
93fn dependency_lines(meta: &YogToml) -> Vec<String> {
94    let mut lines: Vec<String> = meta
95        .dependencies
96        .iter()
97        .map(|(name, spec)| format!("{name} = {spec}"))
98        .collect();
99    for (name, spec) in &meta.mod_dependencies {
100        let exports_name = crate::exports::crate_name(name);
101        let inner = spec
102            .strip_prefix('{')
103            .map(|s| s.trim_end_matches('}').trim());
104        lines.push(match inner {
105            Some("") => format!("{name} = {{ package = \"{exports_name}\" }}"),
106            Some(inner) => format!("{name} = {{ package = \"{exports_name}\", {inner} }}"),
107            None => format!("{name} = {{ package = \"{exports_name}\", version = {spec} }}"),
108        });
109    }
110    lines
111}
112
113/// Yog's crates with test support behind a feature: a mod's tests get it.
114const TEST_SUPPORT: &[&str] = &["yog-kit"];
115const TESTING_FEATURE: &str = "testing";
116
117/// The mod's dependencies that have test support, again for its tests,
118/// with the feature on (`yog-kit = { version = "0.11", features = ["testing"] }`).
119fn test_support(meta: &YogToml) -> Vec<String> {
120    meta.dependencies
121        .iter()
122        .filter(|(name, _)| TEST_SUPPORT.contains(&name.as_str()))
123        .map(|(name, spec)| format!("{name} = {}", with_feature(spec, TESTING_FEATURE)))
124        .collect()
125}
126
127/// A dependency's spec (`"0.11"` or `{ version = .. }`) with `feature` on.
128fn with_feature(spec: &str, feature: &str) -> String {
129    let spec = spec.trim();
130    let features = format!("features = [{feature:?}]");
131    match spec.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
132        Some(inner) if inner.trim().is_empty() => format!("{{ {features} }}"),
133        Some(inner) => format!("{{ {}, {features} }}", inner.trim()),
134        None => format!("{{ version = {spec}, {features} }}"),
135    }
136}
137
138/// The `yog-*` crates a yog-api `Cargo.toml` depends on, in order.
139pub fn yog_dependencies(cargo_toml: &str) -> Vec<String> {
140    let mut in_deps = false;
141    let mut out = Vec::new();
142    for line in cargo_toml.lines() {
143        let line = line.trim();
144        if line.starts_with('[') {
145            in_deps = line == DEPS_SECTION;
146            continue;
147        }
148        if !in_deps {
149            continue;
150        }
151        let Some((name, _)) = line.split_once('=') else {
152            continue;
153        };
154        // `yog-x = { workspace = true }` or the dotted `yog-x.workspace = true`.
155        let name = name.split('.').next().unwrap_or(name).trim();
156        if name.starts_with("yog-") && !out.iter().any(|n| n == name) {
157            out.push(name.to_string());
158        }
159    }
160    out
161}
162
163/// `[patch.crates-io]` pointing every yog-* dependency of the local yog-api
164/// at `api_path`'s sibling crate directory (always including yog-interop,
165/// whose proc-macros generated code must match).
166/// `extra` adds the mod's own yog-* dependencies.
167pub fn local_api_patch(api_path: &Path, extra: &[&str]) -> String {
168    let crates_dir = api_path
169        .parent()
170        .map(Path::to_path_buf)
171        .unwrap_or_else(|| PathBuf::from("crates"));
172    let mut names = std::fs::read_to_string(api_path.join("Cargo.toml"))
173        .map(|t| yog_dependencies(&t))
174        .unwrap_or_default();
175    if !names.iter().any(|n| n == "yog-interop") {
176        names.push("yog-interop".into());
177    }
178    for name in extra {
179        if !names.iter().any(|n| n == name) {
180            names.push((*name).to_string());
181        }
182    }
183    // Transitively: a local yog-ui may need a yog-graphics that crates.io
184    // hasn't got yet.
185    let mut i = 0;
186    while i < names.len() {
187        let deps = std::fs::read_to_string(crates_dir.join(&names[i]).join("Cargo.toml"))
188            .map(|t| yog_dependencies(&t))
189            .unwrap_or_default();
190        for dep in deps {
191            if !names.contains(&dep) {
192                names.push(dep);
193            }
194        }
195        i += 1;
196    }
197    let mut out = String::from("\n[patch.crates-io]\n");
198    for name in names {
199        let dir = crates_dir.join(&name);
200        if !dir.join("Cargo.toml").is_file() && name != "yog-interop" {
201            continue;
202        }
203        let abs = dir.canonicalize().unwrap_or(dir);
204        let _ = writeln!(out, "{name} = {{ path = {:?} }}", abs.to_string_lossy());
205    }
206    out
207}
208
209#[cfg(test)]
210mod tests {
211    #[test]
212    fn yog_dependencies_reads_the_dependencies_table() {
213        let toml = "[package]\nname = \"yog-api\"\n\n[dependencies]\nyog-abi  = \"0.25\"\nyog-ui = { version = \"0.35\" }\nserde = \"1\"\n\n[dev-dependencies]\nyog-test = \"1\"\n";
214        assert_eq!(super::yog_dependencies(toml), ["yog-abi", "yog-ui"]);
215    }
216
217    #[test]
218    fn yog_dependencies_reads_workspace_inherited_lines() {
219        let toml = "[dependencies]\nyog-abi = { workspace = true }\nyog-ui = { workspace = true }\nlibc = { workspace = true }\n";
220        assert_eq!(super::yog_dependencies(toml), ["yog-abi", "yog-ui"]);
221    }
222}