Skip to main content

yog_cli/
cargo_toml.rs

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