1use std::path::{Path, PathBuf};
5
6pub const MANIFEST: &str = "yog.toml";
8pub const BUILD_DIR: &str = ".yog-build";
10pub const DEPS_SECTION: &str = "[dependencies]";
12pub const EXPORT_DEPS_SECTION: &str = "[dependencies.exports]";
14pub const DEFAULT_EDITION: &str = "2024";
16pub const DEFAULT_LICENSE: &str = "MIT OR Apache-2.0";
18const FALLBACK_API_VERSION: &str = "0.96";
20
21pub fn project_root() -> Result<PathBuf, String> {
23 let root = std::env::current_dir().map_err(|e| e.to_string())?;
24 if root.join(MANIFEST).is_file() {
25 Ok(root)
26 } else {
27 Err(format!("no {MANIFEST} found in the current directory"))
28 }
29}
30
31#[derive(Debug)]
33pub struct YogToml {
34 pub id: String,
35 pub name: String,
36 pub version: String,
37 pub description: String,
38 pub name_key: Option<String>,
41 pub description_key: Option<String>,
42 pub icon: Option<String>,
44 pub authors: Vec<String>,
45 pub license: String,
46 pub exports_license: Option<String>,
52 pub datapack: Option<String>,
54 pub edition: Option<String>,
55 pub yog_api_path: Option<String>,
58 pub yog_api_version: Option<String>,
60 pub dependencies: Vec<(String, String)>,
63 pub mod_dependencies: Vec<(String, String)>,
70 pub dev_dependencies: Vec<(String, String)>,
72 pub run_configs: Vec<RunConfig>,
74 pub externals: Vec<External>,
78}
79
80#[derive(Debug, Default, Clone)]
82pub struct External {
83 pub namespace: String,
84 pub ids: Vec<(String, Vec<String>)>,
86}
87
88#[derive(Debug, Default, Clone)]
91pub struct RunConfig {
92 pub name: String,
93 pub display_name: Option<String>,
97 pub export_dir: Option<String>,
100 pub command: Option<String>,
102 pub args: Vec<String>,
104 pub cwd: Option<String>,
106 pub env: Vec<(String, String)>,
108}
109
110impl YogToml {
111 pub fn read(path: &Path) -> Result<Self, String> {
112 let text = std::fs::read_to_string(path)
113 .map_err(|e| format!("reading {}: {e}", path.display()))?;
114 parse_yog_toml(&text)
115 }
116
117 pub fn api_dep(&self) -> String {
124 if let Ok(p) = std::env::var("YOG_API_PATH") {
125 let abs = PathBuf::from(&p)
127 .canonicalize()
128 .unwrap_or_else(|_| PathBuf::from(&p));
129 return format!("yog-api = {{ path = {:?} }}", abs.to_string_lossy());
130 }
131 if let Some(p) = &self.yog_api_path {
132 return format!("yog-api = {{ path = {p:?} }}");
133 }
134 let version = {
135 let env_version = std::env::var("YOG_API_VERSION").ok();
136 self.yog_api_version
137 .as_deref()
138 .or(env_version.as_deref())
139 .map(|v| v.to_owned())
140 .unwrap_or_else(latest_yog_api_version)
141 };
142 format!("yog-api = {:?}", version)
143 }
144}
145
146use yog_data::{Text, Value};
149use yog_formats::manifest::ManifestFile;
150
151fn toml_inline(v: &Value) -> String {
153 let quote = |s: &str| format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""));
154 match v {
155 Value::Map(m) => {
156 let fields: Vec<String> = m
157 .iter()
158 .map(|(k, v)| {
159 let key = if k.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') { k.to_string() } else { quote(k) };
160 format!("{key} = {}", toml_inline(v))
161 })
162 .collect();
163 format!("{{ {} }}", fields.join(", "))
164 }
165 Value::List(items) => format!("[{}]", items.iter().map(toml_inline).collect::<Vec<_>>().join(", ")),
166 Value::Bool(b) => b.to_string(),
167 Value::Int(n) => n.to_string(),
168 Value::Float(f) => f.to_string(),
169 other => quote(other.as_str().unwrap_or_default()),
170 }
171}
172
173fn text_form(t: Text) -> Result<String, String> {
175 match t {
176 Text::Literal(s) => Ok(yog_data::typed::escape_at(&s)),
177 Text::Res(r) => Ok(format!("@{}.{}", r.set, r.key)),
178 Text::NameOf(_) | Text::Lang(_) => Err("a mod's name and description are text or a `.yres` text (`@Strings.mod_name`)".into()),
179 }
180}
181
182pub fn parse_yog_toml(text: &str) -> Result<YogToml, String> {
184 for (i, line) in text.lines().enumerate() {
186 let l = line.trim();
187 if let Some(name) = l.strip_prefix("[run.").and_then(|r| r.strip_suffix(']'))
188 && name.contains('.')
189 && !name.starts_with('"')
190 {
191 return Err(format!("yog.toml:{}: write the run target's name in quotes: [run.\"{name}\"]", i + 1));
192 }
193 }
194 let read = yog_data::schema::from_toml::<ManifestFile>(text).map_err(|e| {
195 e.0.iter()
196 .map(|l| match l.at {
197 Some(at) => format!("yog.toml:{}: {}{}", at.line, if l.path.is_root() { String::new() } else { format!("{}: ", l.path) }, l.message),
198 None => format!("yog.toml: {}", l.message),
199 })
200 .collect::<Vec<_>>()
201 .join("\n")
202 })?;
203 let f = read.value;
204 let m = f.module;
205 let mut dependencies = Vec::new();
206 let mut mod_dependencies = Vec::new();
207 let mut yog_api_version = None;
208 for (name, spec) in f.dependencies.0 {
209 match (name.as_str(), &spec) {
210 ("exports", Value::Map(exports)) => mod_dependencies.extend(exports.iter().map(|(k, v)| (k.to_string(), toml_inline(v)))),
211 ("yog-api" | "yog_api" | "yog_api_version", _) => yog_api_version = Some(spec.as_str().map(str::to_string).unwrap_or_else(|| toml_inline(&spec))),
212 _ => dependencies.push((name, toml_inline(&spec))),
213 }
214 }
215 let run_configs = f
216 .run
217 .0
218 .into_iter()
219 .map(|(name, r)| RunConfig {
220 name,
221 display_name: r.name,
222 export_dir: r.export_dir,
223 command: r.command,
224 args: r.args,
225 cwd: r.cwd,
226 env: r.env.into_iter().filter_map(|kv| kv.split_once('=').map(|(k, v)| (k.to_string(), v.to_string()))).collect(),
227 })
228 .collect();
229 let externals = f
230 .external
231 .0
232 .into_iter()
233 .map(|(namespace, e)| External {
234 namespace,
235 ids: [("items", e.items), ("blocks", e.blocks), ("entities", e.entities), ("recipes", e.recipes)]
236 .into_iter()
237 .filter(|(_, v)| !v.is_empty())
238 .map(|(k, v)| (k.to_string(), v))
239 .collect(),
240 })
241 .collect();
242 let name = m.name.map(text_form).transpose()?;
243 let description = m.description.map(text_form).transpose()?;
244 let icon = m.icon.map(|i| match i {
245 yog_data::Image::Source(s) => s,
246 yog_data::Image::Res(r) => format!("@{}.{}", r.set, r.key),
247 });
248 Ok(YogToml {
249 name: name.unwrap_or_else(|| m.id.clone()),
250 version: m.version.unwrap_or_else(|| "0.1.0".into()),
251 description: description.unwrap_or_default(),
252 name_key: None,
253 description_key: None,
254 icon,
255 authors: m.authors,
256 license: m.license.unwrap_or_else(|| DEFAULT_LICENSE.into()),
257 exports_license: m.exports_license,
258 datapack: m.datapack,
259 edition: m.edition,
260 yog_api_path: f.dev.and_then(|d| d.yog_api_path),
261 yog_api_version,
262 dependencies,
263 mod_dependencies,
264 dev_dependencies: f.dev_dependencies.0.into_iter().map(|(k, v)| (k, toml_inline(&v))).collect(),
265 run_configs,
266 externals,
267 id: m.id,
268 })
269}
270
271pub fn to_snake_name(id: &str) -> String {
277 let mut out = String::with_capacity(id.len());
278 for c in id.chars() {
279 if c == '-' || c == '_' {
280 if !out.ends_with('_') {
281 out.push('_');
282 }
283 } else if c.is_uppercase() {
284 if !out.is_empty() && !out.ends_with('_') {
285 out.push('_');
286 }
287 out.extend(c.to_lowercase());
288 } else {
289 out.push(c);
290 }
291 }
292 out
293}
294
295pub fn workspace_rkyv_version() -> String {
297 env!("RKYV_VERSION").to_string()
298}
299
300pub fn latest_yog_api_version() -> String {
302 match std::process::Command::new("cargo")
303 .args(["search", "yog-api", "--limit", "1"])
304 .output()
305 {
306 Ok(out) if out.status.success() => {
307 String::from_utf8_lossy(&out.stdout)
308 .lines()
309 .next()
310 .and_then(|l| {
311 let mut parts = l.split_whitespace();
312 parts.next(); if parts.next() == Some("=") {
314 parts.next().map(|v| v.trim_matches('"').to_owned())
316 } else {
317 None
318 }
319 })
320 .unwrap_or_else(|| FALLBACK_API_VERSION.into())
321 }
322 _ => FALLBACK_API_VERSION.into(),
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 const VLSI: &str = r#"
333[mod]
334id = "yog-vlsi"
335name = "@Strings.mod_name"
336version = "0.3.0"
337description = "@Strings.mod_description"
338authors = ["F000NK", "Yog Team"]
339license = "AGPL-3.0-only"
340edition = "2024"
341
342[dependencies]
343serde = { version = "1", features = ["derive"] }
344serde_json = "1"
345yog-api = "0.93"
346yog-kit = "0.4"
347
348[dependencies.exports]
349yog-pipes = "0.5"
350
351## a comment
352[run."forge-1.21.1"]
353name = "Forge 1.21.1"
354export_dir = "../Yog-Mod-Loader/forge/run/yog-mods"
355command = "./gradlew"
356args = ["runClient", "-Pminecraft_version=1.21.1"]
357cwd = "../Yog-Mod-Loader/forge"
358env = ["JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64"]
359
360[external.farmersdelight]
361items = ["cutting_board", "tomato"]
362blocks = ["stove"]
363"#;
364
365 #[test]
366 fn reads_a_real_manifest() {
367 let t = parse_yog_toml(VLSI).unwrap();
368 assert_eq!((t.id.as_str(), t.name.as_str(), t.version.as_str()), ("yog-vlsi", "@Strings.mod_name", "0.3.0"));
369 assert_eq!(t.authors, ["F000NK", "Yog Team"]);
370 assert_eq!(t.license, "AGPL-3.0-only");
371 assert_eq!(t.yog_api_version.as_deref(), Some("0.93"), "yog-api is taken out of the dependencies");
372 let deps: Vec<&str> = t.dependencies.iter().map(|(n, _)| n.as_str()).collect();
373 assert_eq!(deps, ["serde", "serde_json", "yog-kit"]);
374 assert_eq!(t.mod_dependencies, [("yog-pipes".to_string(), "\"0.5\"".to_string())]);
375 let run = &t.run_configs[0];
376 assert_eq!((run.name.as_str(), run.display_name.as_deref()), ("forge-1.21.1", Some("Forge 1.21.1")));
377 assert_eq!(run.args, ["runClient", "-Pminecraft_version=1.21.1"]);
378 assert_eq!(run.env, [("JAVA_HOME".to_string(), "/usr/lib/jvm/java-21-openjdk-amd64".to_string())]);
379 let ext = &t.externals[0];
380 assert_eq!(ext.namespace, "farmersdelight");
381 assert_eq!(ext.ids[0], ("items".to_string(), vec!["cutting_board".to_string(), "tomato".to_string()]));
382 }
383
384 #[test]
385 fn problems_say_where() {
386 let e = parse_yog_toml("[mod]\nid = \"m\"\n\n[run.forge-1.21.1]\nname = \"F\"\n").unwrap_err();
387 assert_eq!(e, "yog.toml:4: write the run target's name in quotes: [run.\"forge-1.21.1\"]");
388 let e = parse_yog_toml("[mod]\nid = \"m\"\nautors = [\"me\"]\n\n[external.create]\nitem = [\"wrench\"]\n").unwrap_err();
389 assert_eq!(e.lines().collect::<Vec<_>>(), [
390 "yog.toml:3: mod.autors: unknown field `autors` — did you mean `authors`?",
391 "yog.toml:6: external.create.item: unknown field `item` — did you mean `items`?",
392 ]);
393 let t = parse_yog_toml("[mod]\nid = \"m\"\nname = \"@@me\"\n").unwrap();
394 assert_eq!(t.name, "@@me", "a literal @ stays escaped for resolve_metadata");
395 }
396
397 #[test]
398 fn needs_an_id() {
399 assert!(parse_yog_toml("[mod]\nname = \"x\"").is_err());
400 let t = parse_yog_toml("[mod]\nid = \"m\"").unwrap();
401 assert_eq!((t.name.as_str(), t.version.as_str()), ("m", "0.1.0"), "defaults");
402 }
403}